Update to Scoopbot

2022/07/14

I updated scoopbot to use a database instead of just a list of sites in memory. I have not done anything with Sqlite before, so this was a learning experience for me. Please leave a comment and let me know if you suggest a better way, a more efficient way, or a more Pythonic way, of doing the database stuff.

This change will let it be more robust; for example, previously, if the bot was down and restarted it would ignore anything that was posted in the interim since it rebuilt the list of latest posts when restarted. Now, by checking against the database at a restart, it should have an idea of posts that were added that it missed.

I also used this opportunity to add the ability to have individualized feeds for each user. Also, anybody (who is peered to it on Pestnet) can add feeds in the general channel. This is a privilege, if it gets abused I will change it back to where only I can add feeds.

The new version of scoopbot lives on the Pestnet. If you would like to be added as a peer of scoopbot, please contact me (PeterL on the Pestnet, or leave a comment here).

A change I am thinking of adding is a way to control the bot - with the current version of Blatta, the only way to add peers for the bot is to shut it down and log in to Blatta manually. My idea is to add a table where I can drop in peer, key, and address, and the bot would grab them and add them if they are there.

Here is the code for scoopbot (I will generate a post a vpatch shortly):


#!/usr/bin/python

##############################################################################
## 'Scoopbot', an IRC-RSS feed monitor.                                     ##
##                                                                          ##
## The IRC part of this is based off of 'Watchglass' by asciilifeform,      ##
## see http://www.loper-os.org/?p=3665                                      ##
##                                                                          ##
## (C) 2021, 2022 Peter Lambert ( peterl.xyz )                              ##
##                                                                          ##
## You do not have, nor can you ever acquire the right to use, copy or      ##
## distribute this software ; should you use this software for any purpose, ##
## or copy and distribute it to anyone or in any manner, you are breaking   ##
## the laws of whatever soi-disant jurisdiction, and you promise to         ##
## continue doing so for the indefinite future. In any case, please         ##
## always : read and understand any software ; verify any PGP signatures    ##
## that you use - for any purpose.                                          ##
##############################################################################

import ConfigParser
import Queue
import datetime
import logging
import re
import socket
import sqlite3
import sys
import threading
import time
import urllib2
from datetime import datetime
from xml.etree.ElementTree import XML

##############################################################################
## Config
##############################################################################

# Version. If changing this program, always set this to same # as in MANIFEST

Ver = 744610

cfg = ConfigParser.ConfigParser()

# Single mandatory arg: config file path
if len(sys.argv[1:]) != 1:
    # If no args, print usage and exit:
    print sys.argv[0] + " CONFIG"
    exit(0)

# Read Config
cfg.readfp(open(sys.argv[1]))

# Get log path
logpath = cfg.get("bofh", "log")

# Get IRCism debug toggle
irc_dbg = int(cfg.get("irc", "irc_dbg"))
if irc_dbg == 1:
    log_lvl = logging.DEBUG
else:
    log_lvl = logging.INFO

# Init debug logger:
logging.basicConfig(filename=logpath, filemode='a', level=log_lvl,
                    format='%(asctime)s %(levelname)s %(message)s',
                    datefmt='%d-%b-%y %H:%M:%S')

# Get the remaining knob values:
try:
    # IRCism:
    Buf_Size = int(cfg.get("tcp", "bufsize"))
    Timeout = int(cfg.get("tcp", "timeout"))
    TX_Delay = float(cfg.get("tcp", "t_delay"))
    Servers = [x.strip() for x in cfg.get("irc", "servers").split(',')]
    Port = int(cfg.get("irc", "port"))
    Nick = cfg.get("irc", "nick")
    Pass = cfg.get("irc", "pass")
    Channels = [x.strip() for x in cfg.get("irc", "chans").split(',')]
    Join_Delay = int(cfg.get("irc", "join_t"))
    Discon_TO = int(cfg.get("irc", "disc_t"))

    # Control:
    Prefix = cfg.get("control", "prefix")
    Src_URL = cfg.get("control", "src_url")
    Controlpass = cfg.get("control", "controlpass")

    # RSS Checking:
    CheckTime = int(cfg.get("sites", "checktime"))
    Max_Reported = int(cfg.get("sites", "max_reported"))
    Site_DB = cfg.get("sites", "db")

except Exception as e:
    print "Invalid config: ", e
    exit(1)

##############################################################################
## Feed Handling stuff
##############################################################################

# Used to hold list of sites to send to IRC
NewPosts = Queue.Queue()

# Take a url to a RSS feed and return the title as a string and an array of
# dicts for the items
def parse(my_url):
    # Load data from the given url
    resp = urllib2.urlopen(my_url).read()

    # Find where the xml tree starts and parse the data as an xml structure
    channel = XML(resp[resp.find('<'):])
    site_title = channel.find('./channel/title').text.strip()

    # Convert from XML to a python structure
    items = []
    for k in channel.findall('.//item'):
        items.append({'title': k.find('title').text.strip(),
                      'link': k.find('link').text})

    return site_title, items

def check_rss():
    global NewPosts

    # Open a connection to the database
    DB_Conn = sqlite3.connect(Site_DB)
    cur = DB_Conn.cursor()

    # Continually search for new posts
    while 1:
        cur.execute('SELECT site FROM sites')
        for row in cur.fetchall():
            blog = row[0]
            cur.execute('SELECT user FROM listening WHERE site=?', (blog, ))
            listeners = cur.fetchall()
            try:
                blog_title, posts = parse(blog)
                count = 0
                for item in posts:
                    cur.execute('SELECT 1 FROM pages WHERE site=? AND link=?',
                            (blog, item['link']))
                    if cur.fetchall():
                        break
                    else:
                        count += 1
                        for c in listeners:
                            NewPosts.put([c[0], "%s: [ %s ][ %s ]" % (
                                    blog_title, item['link'], item['title'])])
                        cur.execute('INSERT INTO pages VALUES (?, ?, ?)',
                                (blog, item['title'], item['link']))
                        DB_Conn.commit()

                        if count >= Max_Reported:
                            break

            except Exception as e:
                logging.warning('problem with check of %s : %s' % (blog, e))

        time.sleep(CheckTime * 60)
    DB_Conn.close()

##############################################################################
## IRC Bot
##############################################################################

# Used to compute 'uptime'
time_last_conn = datetime.now()

# Used to monitor disconnection timeout
time_last_recv = datetime.now()

# Socket will be here:
sock = None

# Initially we are not connected to anything
connected = False

# TX Lock for async. operations
irc_tx_lock = threading.Lock()

def init_socket():
    global sock
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Disable Nagle's algorithm for transmit operations
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

    connected = False

def deinit_socket():
    global connected
    global sock
    sock.close()
    connected = False

# Connect to given host:port; return whether connected
def connect(host, port):
    logging.info("Connecting to %s:%s" % (host, port))
    sock.settimeout(Timeout)
    try:
        sock.connect((host, port))
    except (socket.timeout, socket.error) as e:
        logging.warning(e)
        return False
    except Exception as e:
        logging.exception(e)
        return False
    else:
        logging.info("Connected.")
        return True

# Attempt connect to each of hosts, in order, on port; return whether connected
def connect_any(hosts, port):
    for host in hosts:
        if connect(host, port):
            return True
    return False

# Transmit IRC message
def send(message):
    global connected
    if not connected:
        logging.warning("Tried to send while disconnected?")
        return False
    time.sleep(TX_Delay)
    logging.debug("> '%s'" % message)
    message = "%s\r\n" % message
    try:
        irc_tx_lock.acquire()
        sock.send(message.encode("utf-8"))
    except (socket.timeout, socket.error) as e:
        logging.warning("Socket could not send! Disconnecting.")
        deinit_socket()
        return False
    except Exception as e:
        logging.exception(e)
        return False
    finally:
        irc_tx_lock.release()

# Speak given message on a selected channel
def speak(channel, message):
    send("PRIVMSG %s :%s" % (channel, message))

# Standard incoming IRC line (excludes fleanode liquishit, etc)
irc_line_re = re.compile("""^:(\S+)\s+PRIVMSG\s+(\S+)\s+\:(.*)""")

# 'Actions'
irc_act_re = re.compile(""".*ACTION\s+(.*)""")

# input looks like ':PeterL PRIVMSG #pest :!s uptime'
# direct message looks like ':PeterL PRIVMSG scoopbot :just checking'

# A line was received from IRC
def received_line(line):
    # Process the traditional pingpong
    if line.startswith("PING"):
        send("PONG " + line.split()[1])
    else:
        logging.debug("< '%s'" % line)
        standard_line = re.search(irc_line_re, line)
        if standard_line:
            # Break this line into the standard segments
            (user, chan, text) = [s.strip() for s in standard_line.groups()]
            # Handle direct messages
            if chan == Nick:
                chan = user
            # Determine whether this line is an 'action' :
            action = False
            act = re.search(irc_act_re, line)
            if act:
                action = True
                text = act.group(1)
                # Remove turd if present:
                if text[-1:] == '\x01':
                    text = text[:-1]
            # This line is edible, process it.
            eat_line(user, chan, text, action)

joined = False

def join_chans():
    global joined
    if joined:
        return
    for chan in Channels:
        logging.info("Joining channel '%s'..." % chan)
        send("JOIN %s\r\n" % chan)
        joined = True

# IRCate until we get disconnected
def irc():
    global connected
    global time_last_conn
    global time_last_recv
    global sock
    global NewPosts

    # Initialize a socket
    init_socket()

    # Connect to one among the specified servers, in given priority :
    while not connected:
        connected = connect_any(Servers, Port)

    # Save time of last successful connect
    time_last_conn = datetime.now()

    # Auth to server
    # If this is a production bot, rather than test, there will be a PW:
    if Pass != "":
        send("PASS %s\r\n" % Pass)
    send("NICK %s\r\n" % Nick)
    send("USER %s %s %s :%s\r\n" % (Nick, Nick, Nick, Nick))

    time.sleep(Join_Delay)  # wait to join until server eats auth

    lnrec = 0
    while connected:
        try:
            data = sock.recv(Buf_Size)
            time_last_recv = datetime.now()  # Received anything -- reset timer
            lnrec = lnrec + 1
            if lnrec > 2:
                join_chans()
            if not NewPosts.empty():
                m = NewPosts.get()
                speak(m[0], "New post on %s" % (m[1]))

        except socket.timeout as e:
            logging.debug("Receive timed out")
            # Determine whether the connection has timed out:
            since_recv = (datetime.now() - time_last_recv).seconds
            if since_recv > Discon_TO:
                logging.info("Exceeded %d seconds of silence " % Discon_TO
                             + "from server: disconnecting!")
                deinit_socket()
            continue
        except socket.error as e:
            logging.warning("Receive socket error, disconnecting.")
            deinit_socket()
            continue
        except Exception as e:
            logging.exception(e)
            deinit_socket()
            continue
        else:
            if len(data) == 0:
                logging.warning("Receive socket closed, disconnecting.")
                deinit_socket()
                continue
            try:
                try:
                    data = data.strip(b'\r\n').decode("utf-8")
                except UnicodeDecodeError:
                    data = data.strip(b'\r\n').decode('latin-1')
                for l in data.splitlines():
                    received_line(l)
                continue
            except Exception as e:
                logging.exception(e)
                continue

##############################################################################

# Commands:

def cmd_add(arg, user, chan):
    # Add a feed for the chan
    site = arg.split(' ')[0].strip()
    try:
        site_title, articles = parse(site)
    except Exception as e:
        logging.exception(e)
        speak(chan, "Could not add site %s" % site)
        return

    # check to see if the site is in the db yet
    DB_Conn = sqlite3.connect(Site_DB)
    cur = DB_Conn.cursor()

    cur.execute('SELECT * FROM sites WHERE site=?', (site, ))
    if not cur.fetchall():
        cur.execute('INSERT INTO sites VALUES (?, ?)', (site, site_title))

        # update the latest articles
        for item in articles:
            cur.execute('INSERT INTO pages VALUES (?, ?, ?)',
                    (site, item['title'], item['link']))

    # make sure that this is not a duplicate
    cur.execute('SELECT * FROM listening WHERE user=? AND site=?', (chan, site))
    if not cur.fetchall():
        cur.execute('INSERT INTO listening VALUES (?, ?)', (chan, site))

        if articles:
            speak(chan, "Added site %s, %s, latest: %s" %
                    (site, site_title, articles[0]['link']))
        else:
            speak(chan, "Added site %s with no articles found yet." % site)

    else:
        speak(chan, "%s: It looks like you are already following %s" %
                (user, site))

    DB_Conn.commit()
    DB_Conn.close()

def cmd_list(arg, user, chan):
    # list feeds associated here
    DB_Conn = sqlite3.connect(Site_DB)
    cur = DB_Conn.cursor()
    cur.execute('SELECT site FROM listening WHERE user=?', (chan, ))
    sites = cur.fetchall()
    if sites:
        for row in sites:
            speak(chan, '%s' % row[0])
    else:
        speak(chan, 'no feeds followed here')
    DB_Conn.close()

def cmd_help(arg, user, chan):
    # Speak the 'help' text
    speak(chan, "%s: my valid commands are: %s" %
          (user, ', '.join(Commands.keys())))

def cmd_remove(arg, user, chan):
    site = arg.split(' ')[0].strip()

    # drop from table listening
    DB_Conn = sqlite3.connect(Site_DB)
    cur = DB_Conn.cursor()
    cur.execute('DELETE FROM listening WHERE user=? AND site=?', (chan, site))
    DB_Conn.commit()

    # if no users are following site, drop from sites
    cur.execute('SELECT 1 FROM listening WHERE site=?', (site, ))
    if not cur.fetchall():
        cur.execute('DELETE FROM sites WHERE site=?', (site, ))
        DB_Conn.commit()

    DB_Conn.close()

def cmd_src(arg, user, chan):
    speak(chan, "%s: my source code can be seen at: %s" % (user, Src_URL))

def cmd_version(arg, user, chan):
    speak(chan, "I am 'Scoopbot' version %s." % Ver)

def cmd_uptime(arg, user, chan):
    uptime_txt = ""
    uptime = (datetime.now() - time_last_conn)
    days = uptime.days
    hours = uptime.seconds / 3600
    minutes = (uptime.seconds % 3600) / 60
    uptime_txt += '%dd %dh %dm' % (days, hours, minutes)
    # Speak the uptime
    speak(chan, "%s: time since my last reconnect : %s" %
          (user, uptime_txt))

Commands = {
    "help": cmd_help,
    "version": cmd_version,
    "src": cmd_src,
    "uptime": cmd_uptime,
    "list": cmd_list,
    "add": cmd_add,
    "remove": cmd_remove
}

##############################################################################

# All valid received lines end up here
def eat_line(user, chan, text, action):
    # If the line was a command for this bot, process; otherwise ignore.
    if text.startswith(Prefix):
        cmd = text.partition(Prefix)[2].strip()
        cmd = [x.strip() for x in cmd.split(' ', 1)]
        if len(cmd) == 1:
            arg = ""
        else:
            arg = cmd[1]
        # Dispatch this command...
        command = cmd[0]
        logging.debug("Dispatching command '%s' with arg '%s'.." %
                      (command, arg))
        func = Commands.get(command)
        # If this command is undefined:
        if func is None:
            logging.debug("Invalid command: %s" % command)
            # Utter the 'help' text as response to the sad command
            cmd_help("", user, chan)
        else:
            # Is defined command, dispatch it:
            func(arg, user, chan)

##############################################################################

# IRCate; if disconnected, reconnect
def run():
    while 1:
        irc()
        logging.warning("Disconnected, will reconnect...")

# Run continuously.
ircer = threading.Thread(target=run, args=())
ircer.start()

scooper = threading.Thread(target=check_rss, args=())
scooper.start()

ircer.join()
scooper.join()

Pest Network Example MetaPost Figures

2022/01/13

Recently in chat there was a link to MetaPost. I thought I would try it out, here is an example of some figures that I created with just a few minutes of tinkering to get an idea of how to use the program.

--

The Pest spec has been introduced as a replacement to tradional IRC, you can check it out over at Loper-OS.org. One of the factors that impelled its development was the way that the traditional IRC protocol limits server arrangements to a "binary spanning tree", meaning you can link servers in chains, but the server chains cannot form cycles. For example, servers A, B, C, and D can be linked thus:

layouts-3

The problem that happens is whenever servers A and B lose their connection there is a "netsplit", where A and C can talk, and B and D can talk, but the two sides cannot talk together. Pest uses duplicate message elimination to allow the formation of cyclic networks, like the one shown below:

layouts-4

This way, if A and B lose their connection there is still communication between all members of the network.

--

This is the code I used to generate the figures above using MetaPost. I make no claim that this is the best way to do this, it is just an example of what the code for this looks like.

outputtemplate := "%j-%c.svg";
outputformat   := "svg";
beginfig (1);
  numeric u;
  u := 1cm;

  % Put in some labels
  label ("A", (0, 2u)) ;
  label ("B", (4u,2u)) ;
  label ("C", (0, 0 )) ;
  label ("D", (4u,0 )) ;

  % Connecting lines to show the network
  draw (0, .5u) -- (0, 1.5u) ;
  draw (.5u,2u) -- (3.5u,2u) ;
  draw (4u,.5u) -- (4u,1.5u) ;

  % Edges of labels are getting cut off, this is an ugly hack to
  % expand the picture a tad
  draw (-.5u,2.5u) -- (4.5u,2.5u) withcolor white;

endfig;

beginfig (2);
  numeric u;
  u := 1cm;

  label ("A", (0, 2u)) ;
  label ("B", (4u,2u)) ;
  label ("C", (0, 0 )) ;
  label ("D", (4u,0 )) ;

  draw (0, .5u) -- (0,   1.5u) ;
  draw (.5u,2u) -- (3.5u,2u  ) ;
  draw (4u,.5u) -- (4u,  1.5u) ;
  draw (.5u,0 ) -- (3.5u,0   ) ;
  draw (.5u,.25u) -- (3.5u,1.75u) ;
  fill fullcircle scaled (2/5u) shifted (2u,1u) withcolor white ;
  draw (.5u,1.75u) -- (3.5u,.25u) ;

  draw (-.5u,2.5u) -- (4.5u,2.5u) withcolor white;

endfig;

end.

--

Edit:

Question was asked, can the graph be made with arrows instead?

Answer: Quite easily. Just replace the edges, instead of draw, use drawdblarrow.

Updated figures:
layouts-11 layouts-21

From the Spam Trap

2022/01/07

I just added a math spam filter (thanks for posting this, Asciilifeform!), I have not been getting spam frequently but since I do not post very often there was over 200 caught since the last time I looked.

Here is an interesting comment that got caught in the spam trap, it must be important since it was so long, right? I have no idea what it means though. And this is only the first little bit, maybe 10%, it just keeps going on and on like this!

find education summer camp

My page :: Finquiz Cfa Level 1 Mock Exam Finquiz Cfa Cfa Level 1 Summary Notes Finquiz Login Fin Quiz Cfa Level 1 Formulas Cfa Level 1 Mock Exam Pdf Free Cfa Level 1 Formula Sheet Cfa Curriculum 2020 Pdf Cfa Level 2 Mock Exam Finquiz Cfa Level 1 Mock Exam Finquiz Cfa Review Cfa Level 2 Syllabus Pdf Cfa Level 1 Books Pdf Cfa Level 2 Curriculum Pdf Free Cfa Study Material Cfa Level 1 Practice Questions Pdf Cfa Curriculum 2021 Pdf Cfa Level 1 Pdf Cfa Level 1: Essential Formulas All Cfa Level 1 Formulas Cfa Level 3 Mock Exam Cfa Level 1 Mock Exam Pdf With Answers Cfa Syllabus 2021 Pdf Free Cfa Level 1 Mock Exam Cfa Level 2 Mock Exam 2021 Pdf Cfa Level 1 Question Bank Free Cfa Level 1 Mock Exam 2021 Cfa Level 1 Notes Pdf Cfa Level 3 Notes Cfa Level 1 Mock Exam Free Cfa Level 2 Summary Notes Cfa Level 1 Mock Exam Pdf Cfa Level 2 Notes Cfa Level 1 Formula Sheet 2021 Finquiz.Com Cfa Level 1 Study Material Pdf Cfa Level 3 Curriculum Pdf Cfa 2021 Curriculum Pdf Cfa Mock Exam Cfa Level 1 Notes Schweser Cfa Level 2 2021 Free Download Cfa Level 3 Summary Notes Cfa Level 2 Mock Exam 2021 Cfa Mock Exam Pdf Cfa Mock Exam Level 1 Cfa Level 2 Mock Exams Cfa Level 2 Mock Exam Pdf Cfa Mock Exam Level 1 Pdf Cfa Summary Notes Cfa Level 2 Question Bank Free Download Pdf Cfa Level 1 Mock Exams Cfa Level 2 Curriculum 2020 Pdf Cfa Level 3 Practice Exams Cfa Level 1 Study Material Cfa Level 2 Formula Sheet Cfa Level 2 Question Bank Free Download Cfa Level 3 Mock Exams Cfa Level 1 Free Mock Exam Cfa Level 2 Study Material Pdf Finuiz Cfa Level 1 Question Bank Free Download Pdf Cfa Level 1 Question Bank Cfa Mock Test Free Cfa Level 2 Mock Cfa Level 1 Questions Bank Free Cfa Level 1 All Formulas Cfa Level 1 Summary Cfa Level 1 Free Study Material Cfa Level 1 Curriculum Changes 2021 Cfa Level 1 Free Mock Test Cfa Level 2 Question Bank Pdf Cfa Level 1 Formula Cfa Level 1 Books 2021 Pdf Cfa Mock Exam Free Cfa Formula Sheet Level 1 Cfa Level 1 Mock Exam 2020 Pdf With Answers Cfa Level 2 Curriculum Changes 2021 Cfa Level 1 Book Pdf Cfa Mock Exam Level 2 Cfa Level 2 Curriculum Free Download Cfa Level 3 Notes Pdf Formulas Cfa Level 1 Cfa Level 1 Question Bank Pdf Cfa Level 1 Curriculum Pdf Cfa Level 3 Pdf Cfa Pdf 2021 Cfa Level 1 2022 Curriculum Pdf Cfa Study Material Pdf Cfa Level 1 Material Pdf Cfa Level 1 Curriculum 2022 Pdf Cfa Level 1 Study Material Pdf 2020 Cfa Level 2 Practice Questions Pdf Cfa Level 1 Syllabus Pdf Cfa Level 1 Material Cfa Level 2 Practice Exams Cfa Formulas Cfa Level 3 Syllabus 2021 Cfa Level 2 Practice Exams Free Download Cfa Level 1 Mock Exam 2020 Pdf Cfa Level 1 Practice Questions Free Cfa Level 3 Formula Sheet Cfa Level 1 2022 Curriculum Pdf Download ข้อสอบ Cfa Level 1 Pdf Cfa 2021 Pdf Cfa Level 2 Mock Exam Free Cfa Level 1 Books Download Cfa Level 3 Question Bank Free Download Finquiz Mock Exam Cfa Level 1 Practice Exams Cfa Level 2 Practice Questions Cfa Level 1 Revision Notes Cfa Pdf Cfa Books Pdf Mock Exam Cfa Level 1 Cfa Level 2 Syllabus 2021 Cfa Curriculum 2022 Pdf Cfa Study Materials Free Cfa Level 3 Question Bank Cfa 2022 Curriculum Pdf Cfa Level 2 Formula Sheet 2021 Cfa Level Ii Mock Exam Free Cfa Mock Exams Level 1 Cfa Study Guide Pdf Cfa Free Mock Exam Level 1 Cfa Question Bank Cfa Curriculum Pdf Cfa Reading List Cfa Level 1 Syllabus 2021 Pdf Schweser Cfa Level 3 2021 Free Download Cfa Level 1 Mock Exam Pdf With Answers Free Cfa Level 2 Notes Pdf Cfa Level 1 Changes 2020 To 2021 Schweser Cfa Level 1 2021 Pdf Cfa Study Material 2021 Cfa Study Material Free Download Cfa Level 1 Curriculum Changes Cfa Level 3 Study Material Cfa Formula Cfa 2021 Curriculum Changes Cfa Book Pdf Cfa Level 1 2021 Curriculum Changes Cfa Notes Pdf Cfa Level 1 Curriculum 2021 Pdf Cfa Level 1 2021 Mock Exam Cfa Level 1 Download Mock Exam Cfa Level 2 Schweser Cfa Level 2 Practice Exams Pdf Free Cfa Level 2 Mock Exam Cfa 2020 Curriculum Pdf Cfa Level 1 2021 Pdf Cfa Level 2 Changes 2019 To 2020 Cfa Level 1 Books Free Cfa Material Download

A funny thing about Python

2021/09/19

Python has a way to convert arbitrary data types to Boolean (True/False). For example, an integer of value 0 will be converted to False, while any other integer will be converted to True. Empty sets are converted to False, sets that contain things are converted to True. So here are a few examples, as shown in a REPL (read, evaluate, print, loop) session:

>>> bool(0)
False
>>> bool(1)
True
>>> bool(2)
True
>>> bool([])
False
>>> bool([1])
True
>>> bool([1, 2])
True
>>> bool(True)
True
>>> bool(False)
False
>>> bool('True')
True

These should all make sense. Now for the funny part. Observe the REPL output below, which seems to give the wrong answers:


>>> bool('False')
True
>>> bool([False])
True
>>> bool([[]])
True

Of course, the first one is True because the interpreter is just checking to see if the string is empty or not, and it does not care what is in the string. Similarly, in the second and third examples the interpreter is just checking to see if there is an empty set. The False and the empty set are each taking up space and so the sets are not empty.

Python does give a way to check arrays, using the any and all built-in functions. Some examples:

>>> any([False, False])
False
>>> any([True, False])
True
>>> all([True, False])
False
>>> all([True, True])
True

One place to be careful, though, is with empty sets. Observe the following:

>>> any()
Traceback (most recent call last):
  File "<pyshell#75>", line 1, in 
    any()
TypeError: any() takes exactly one argument (0 given)
>>> any([])
False
>>> any([[]])
False
>>> any([[[]]])
True

I included the first to show that there is an error if you do not supply any set at all. The second is an empty set, which returns false since it does not contain anything. The third one is false since the innermost empty set contains one empty set, which converts to False (as we saw above), while the fourth one has a set containing a non-empty set, therefore it is equivalent to any([True]), which returns the expected True as seen above.

The Sportsman's Antics

2021/09/16

The day is spent in nervous anticipation.
As twilight falls, you move to your appointed place.
Darkness hides you, surrounds you,
A light beckons to you; a figure draws your attention.
Amid the roaring crowd you approach.
You size them up, they seem so familiar,
Each night a different person.
The crowds fade away,
The two of you are surrounded by a vast chasm.
You do the dance that is required.
Sweaty bodies clash, pushing, pulling.
Emotions run high, but it is merely for show,
All too soon it is over.
You walk down the dark and empty street,
Wearily, you head for home.

---

This poem was originally written in 2005 for a humanities class assignment, one of those things where you have to write a poem and then analyze your own poem. I happened to come across it while going through my old files, so I decided to keep it here.

Special Delivery by Kris Neville

2021/08/21

I just read the book Special Delivery, by Kris Neville (originally published in 1951). This is an interesting science fiction story, where an alien race is secretly invading the Earth. These aliens have the technology to replicate anything from Earth, and their magnificent plan to soften up the Earth for the invasion amounts to mailing a stack of money to each and every person. Of course this works as the aliens intended: everybody gleefully takes the stacks of money. This leads to immediate shortages as people decide they don't have to work anymore, then rioting and pillaging as people try to get everything for themselves before it is all gone, and then general societal collapse.

Perhaps you can see where I am going here?

Recently in the US, due to the Covid Pandemic, there have been various "stimulus payments" and "increased unemployment payments" and "monthly child tax credits" which all amount to mailing stacks of money to the general public. We should be aware that we are playing with fire here. It seems like there are "help wanted" signs popping up everywhere. Inflation has gotten to the point that people are starting to notice it. There seem to be new shortages of seemingly random items each month. But throughout all this, nobody is stopping to ask the question of whether this is a good idea to hand out free money to everybody, they just keep asking for more! Instead, the media is embroiled in covering the nitty gritty of who picks which side in the manufactured controversy of who "believes in science" more, whether it is the Team Red or the Team Blue and whether they will mandate masks or vaccines or forbid mandates of one or the other, all while breathlessly calling the other side names and completely ignoring the sinking ship of the economy as it tips over the brink of disaster.

The new and improved Scoopbot

2021/07/28

So I threw together an IRC bot (sig) for monitoring RSS feeds and announcing when new blog posts show up. A few years ago I had made one of these, but I seem to have misplaced the library it used for parsing RSS feeds. I started with the IRC bot structure found in Watchglass. I built a little function to pull out the stuff I wanted from the RSS feeds, so now I don't need that external library anymore.

Currently it just announces to all channels anything in the blog list. In the future I want to alter it so that it can take PM's to subscribe individually to feeds. If you have other ideas, please comment below. I also appreciate critiques of my code, whether it is something that I could have done more efficiently or if there is something that is just plain dumb.

Update: Had to change the program a little, I moved where it speaks the posts to a part of the loop that actually gets hit; it was hidden behind a 'continue' previously. Also changed the logic for storing the recent feeds to hold a few recent posts instead of just one, so that if the most recent post is deleted the program will still recognize the older posts and not report them as new.

Update 2: Added blog titles to the information parsed from the RSS and reported to IRC. Patch scoopbot_add_titles.vpatch (sig)

Overview of the Paleo Diet

2021/07/20

The other day I cam across an article about nutrition, and they quoted some guy who had spent time studying a hunter-gatherer society in Africa. This part got me thinking:

If you go out and have a chance to live with a group like the Hadza, you realize that a lot of the stories we tell ourselves about the past, including things like the paleo diet, just kind of fall apart. So there's this idea in the paleo diet world that there's one sort of single natural human diet, and that diet was very meat heavy, hardly any carbs at all and certainly no sugars.

[In reality] the Hadza have a mix of plants and animals in their diets. It changes day to day and year to year, but about half of the calories are coming from plants.

Clearly this guy does not understand what the Paleo Diet is. He has just summarized the Paleo Diet while trying to debunk it. The Paleo diet is not like the Atkins or South Beach diets where you drop everything except meat and then snobbily lecture your friends about your amazing diet while snarfing two pounds of bacon. Instead, the paleo diet focuses on a balance of a small amount of lean meat with an abundance of fruits and vegetables.

A while ago my wife and I went through learning the paleo diet, and more than any particular rules about eat this and not that, it was beneficial for changing the overall way we approached eating and menu planning. Before we studied the paleo diet, if we were at home and hungry and wanted something quick, we would throw on a pot of pasta, cover it in butter and call that dinner. Now, I realize that there are so much better things to eat, both in leaving you feeling better when you are done and also just having better flavors.

So here is a quick summary of the paleo diet:

  • Avoid processed foods - eat fresh stuff.

  • Avoid eating out, cook things yourself.
  • Consume a small to moderate amount of lean meats. Things like fish, a 4 oz steak (not a 24 oz steak), or a grilled chicken breast (not breaded and fried grease bomb with a bit of reconstituted chicken in the center).
  • Eat lots of vegetables, and use a wide variety.
  • Moderate amounts of fruits, berries, and nuts; these are great for adding in more flavors and variety to various recipes.
  • Low or no dairy (some people say none at all, some people say limit to small amounts) - this is one where it helps to do a "cleanse", go a few weeks or a month without it and see how you feel. It is widely documented citation needed? that some populations (like most Europeans) have a gene that allows them to process dairy while other populations (like most Asians) lack that gene and are therefore lactose intolerant, but humans have a tendency to be migratory and promiscuous so you might not be in the group that you think. In other words, try going without dairy and see if you feel a huge difference.
  • Most paleo guides say "no bread" or even "no grains", but this is one where I tend to temper it down to using moderation and less processing. So it is better to use brown rice instead of white rice, and whole grain bread instead of cheapo white bread. Corn-on-the-cob is great, a corn chip (like a Cheeto) is not so good.
  • Eggs are good too.

Overall, using the paleo diet is about rejecting the "progress" that has happened to food, where we are constantly being sold something more bland than we had before. While industrialized agriculture allows growing a huge amount of high fructose corn syrup, and that gives people the food energy they need to live, it does not give the vitamins and minerals that you need to live healthy. I recently saw a commercial for a liquid food replacement named "huel", which to me sounds like a portmanteau of hurl and gruel. Part of me wanted to hurl, part of me wanted to ask if this is some sort of sick joke, and part of me is curious to see who the hell would want that sort of garbage?

The Simple Diet

Somewhat related, I once went through a list of all the vitamins that people need, and made a list of the types of foods that provide each vitamin. Working on the premise that your body knows when it is missing something and so you will be hungry until you fill that need, my hypothesis is that eating something out of each of these categories will help you be healthier and avoid that craving which induces overeating. So here are the categories:

  • Green (leafy) vegetables, such as spinach, broccoli, kale

  • Colorful vegetables, such as carrots, peppers, sweet potatoes
  • Fruits
  • Meat and eggs
  • Whole grains
  • Beans and nuts
  • "Good oils", things like olives, avocados, and fish

So the idea is that if you focus on making sure you get at least a little bit from each of these categories you don't have to count any calories or anything, your body will just have access to the things it needs and you will be healthier.

US VP Debate 2020

2020/10/08

I suppose since I wrote up a review of the first presidential debate I should continue with what happened at the vice-presidential debate as well.

The current vice president, Mike Pence, squared off against the Democrat challenger, Senator Kamala Harris of California. Unlike the presidential debate, this one was mostly civil. More often than not, Pence continued talking long after his time was up, but at least there was no shouting at each other. In fact, they might have gone too far the other direction; there was so little energy that it was fairly boring.

The biggest thing that stuck out to me was that both candidates, but especially Harris, did not always answer the questions asked. There were a couple times where it seemed like Harris hadn't prepped for a certain question, so she just went ahead and answered a different question that she wished they had asked.

The topics covered were very similar to those covered in the presidential debate, so much of this one felt like a rehashing of the same talking points, just from a weaker point of view ("my guy is going to do ..." instead of "I am going to do ..."). The topic of whether the Democrats would try adding extra supreme court justices was again asked, and again the Democrat refused to address the question. Isn't that why we have the debates, so people know where you stand on issues? Why would they refuse to answer the question, unless they don't think voters would like the answer they give? What a non-answer should tell you is that voting for this guy would give you the opposite result of what you want - left-wingers should proceed as if she said "Our supreme court has had nine members for hundreds of years, we are not going to change it", and right-wingers should proceed as if she answered "We are going to nominate five of the most liberal judges we can find on our first day in office".

So instead of answering the question asked about the supreme court, Harris said something like "You want to talk about court packing, of the 30 federal judges appointed to life-time positions by this administration not a single one of them was Black!". Now, Pence did not really get a chance to answer, but it only takes a second to remember that on the current Supreme Court there is only one Black guy, and he was nominated by a Republican. Would Harris have been satisfied if a clone of Clarence Thomas had been nominated to the Appeals court? Furthermore, unlike Biden, Trump has published a list of people he would consider nominating to the supreme court, and it does include a couple Black guys.

The way the debate was formatted did not allow a lot of conversation between the candidates, but over the course of a couple questions this general back-and-forth happened:

Pence: The Trump administration passed a tax cut that saved the Average American Family1 $2000 per year on income taxes.
...
Harris: On day one of the Biden administration we are going to repeal the Trump tax cuts2.
Pence: There you have it, she said they are going to raise taxes.
Harris: I never said that!
Pence: You just said you are going to repeal a tax cut, which is the same as passing a tax increase.
Harris: Under a Biden administration taxes will not go up for anybody making under $400,000 a year.

Wait, where did we suddenly pull the number $400,000 from? Is that supposed to be the top of the middle class or something? I seem to remember Barak Obama promising not to raise taxes on anybody making below $250,000, and even then I thought the number was a bit on the high side. I guess that is inflation for you.

One interesting thing I noticed was the way the VP candidates referred to their presidential candidates. Pence always called him "President Trump", as if trying to emphasize his current position, perhaps to build up the image of a respectable person that voters could more easily support than the boorish name-caller that we saw on display during the last debate. Harris, on the other hand, was constantly calling her running mate "Joe", as if the playbook from their campaign is to emphasize his down-to-earth persona, somebody you could have a beer with, as they say. Marketing, it's all just marketing.

Overall, I would say that both candidates came across as sleazy politicians who play loose with the facts, but I disliked Harris more. For as much as she was touted as a former DA who could handle herself in a debate, she was underwhelming and fell far below my expectations. Pence came across as the kind of guy who has convictions and will go ahead and quietly do whatever it is that he wants to do (a stark contrast to the president, who picks random directions of travel but does it as loudly and crassly as possible).

  1. This mythical family of four makes something like $100,000 a year. []
  2. Nevermind that the president can't actually do that, it would take an act of Congress to change the tax code, but people running for president usually ignore that fact. []

Sad rant from a horny teenager

2020/10/01

The other day I was driving with my teen-aged daughter who was in a sad, weepy mood. I asked her what was bothering her. She started to sob and said to me "I am so stressed because I am so horny all the time. I see a cute boy and my body says 'I would fuck that', but I don't even know how to fuck!" Sob, sob, sob ...

This is the same daughter who struggles with tampons because, as she says, "I know it's supposed to go in my vagina, but I can't figure out where my vagina is."

The struggle is real.