2018-11-01 11:05:52 -04:00
|
|
|
#!/usr/bin/python3
|
2018-06-13 14:34:23 +00:00
|
|
|
# http://wiki.shellium.org/w/Writing_an_IRC_bot_in_Python
|
|
|
|
|
|
|
|
|
|
# Import some necessary libraries.
|
|
|
|
|
import socket
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
2018-11-01 11:05:52 -04:00
|
|
|
import argparse
|
2018-06-13 14:34:23 +00:00
|
|
|
import fileinput
|
|
|
|
|
import random
|
|
|
|
|
import time
|
|
|
|
|
import re
|
|
|
|
|
import operator
|
|
|
|
|
|
2018-11-01 11:05:52 -04:00
|
|
|
import util
|
2018-06-13 14:34:23 +00:00
|
|
|
import madlib
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
class State:
|
2018-10-05 16:02:38 -04:00
|
|
|
idle = 0 # When the bot is started and after it has finished a story
|
|
|
|
|
thinking = (
|
|
|
|
|
1
|
|
|
|
|
) # I intentionally speed throttle this bot so it feels a little more natural
|
|
|
|
|
story_selection = 2 # When the bot is waiting for a user input for story selection
|
|
|
|
|
word_input = 3 # When the bot is waiting for user input after prompted for a word
|
|
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
|
|
|
|
|
# Globals
|
2018-10-05 16:02:38 -04:00
|
|
|
MAX_LINE = 80 # The maximum number of characters in a line
|
|
|
|
|
MAX_STORIES = 5 # The maximum number of stories a user can pick from
|
|
|
|
|
THROTTLE_FACTOR = 1 # A factor by which all sleep event durations will be multiplied
|
2018-10-05 13:56:15 -04:00
|
|
|
# Allow madlbibbot to run multiple simultaneous stories
|
2018-10-05 16:02:38 -04:00
|
|
|
state = {} # The current state of the bot
|
|
|
|
|
stories = {} # The list of stories available to users
|
|
|
|
|
story = {} # The madlib story currently being worked on
|
|
|
|
|
nextword = {} # The word that the bot is currently expecting data for
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-11-01 11:05:52 -04:00
|
|
|
parser = argparse.ArgumentParser()
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-11-01 11:05:52 -04:00
|
|
|
parser.add_argument(
|
2018-10-05 16:02:38 -04:00
|
|
|
"-s",
|
|
|
|
|
"--server",
|
|
|
|
|
dest="server",
|
2018-10-08 17:19:07 -04:00
|
|
|
default="127.0.0.1:6667",
|
2018-10-05 16:02:38 -04:00
|
|
|
help="the server to connect to",
|
|
|
|
|
metavar="SERVER",
|
|
|
|
|
)
|
2018-11-01 11:05:52 -04:00
|
|
|
parser.add_argument(
|
2018-10-05 16:02:38 -04:00
|
|
|
"-c",
|
2018-11-01 11:05:52 -04:00
|
|
|
"--channels",
|
|
|
|
|
nargs="+",
|
|
|
|
|
dest="channels",
|
|
|
|
|
default=["#madlibs"],
|
|
|
|
|
help="the channels to join",
|
|
|
|
|
metavar="CHANNELS",
|
2018-10-05 16:02:38 -04:00
|
|
|
)
|
2018-11-01 11:05:52 -04:00
|
|
|
parser.add_argument(
|
2018-10-05 16:02:38 -04:00
|
|
|
"-n",
|
|
|
|
|
"--nick",
|
|
|
|
|
dest="nick",
|
|
|
|
|
default="madlibbot",
|
|
|
|
|
help="the nick to use",
|
|
|
|
|
metavar="NICK",
|
|
|
|
|
)
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-11-01 11:05:52 -04:00
|
|
|
args = parser.parse_args()
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
def resetGlobals(channel=""):
|
|
|
|
|
global state
|
|
|
|
|
global stories
|
|
|
|
|
global story
|
|
|
|
|
global nextword
|
|
|
|
|
|
|
|
|
|
if channel == "":
|
|
|
|
|
state = {}
|
|
|
|
|
stories = {}
|
|
|
|
|
story = {}
|
|
|
|
|
nextword = {}
|
|
|
|
|
else:
|
|
|
|
|
state[channel] = State.idle
|
|
|
|
|
stories[channel] = []
|
|
|
|
|
story[channel] = ""
|
|
|
|
|
nextword[channel] = {}
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-09-19 17:04:38 +00:00
|
|
|
def get_stories(channel, botnick):
|
2018-06-13 14:34:23 +00:00
|
|
|
global state
|
|
|
|
|
global stories
|
|
|
|
|
state[channel] = State.thinking
|
|
|
|
|
stories[channel] = madlib.find_stories(MAX_STORIES, True)
|
2018-11-01 11:05:52 -04:00
|
|
|
if len(list(stories[channel])) == 0:
|
|
|
|
|
util.sendmsg(ircsock, channel, "Uh oh! There are no stories!")
|
2018-06-13 14:34:23 +00:00
|
|
|
state[channel] = State.idle
|
|
|
|
|
else:
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "Here are a couple good stories:")
|
2018-06-13 14:34:23 +00:00
|
|
|
time.sleep(1 * THROTTLE_FACTOR)
|
|
|
|
|
for idx, story in enumerate(stories[channel]):
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "[{}] {} ({} words)".format(idx, story[0], story[2]))
|
2018-06-13 14:34:23 +00:00
|
|
|
time.sleep(0.5 * THROTTLE_FACTOR)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock,
|
2018-10-05 16:02:38 -04:00
|
|
|
channel,
|
|
|
|
|
"Please select a story by index by saying '{}: <number>':".format(botnick),
|
|
|
|
|
)
|
2018-06-13 14:34:23 +00:00
|
|
|
state[channel] = State.story_selection
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Handle user input when the bot is directly addressed
|
2018-09-19 17:04:38 +00:00
|
|
|
def handle_bot_msg(channel, msg, botnick):
|
2018-06-13 14:34:23 +00:00
|
|
|
global state
|
|
|
|
|
global stories
|
|
|
|
|
|
|
|
|
|
if channel not in state:
|
|
|
|
|
state[channel] = State.idle
|
|
|
|
|
|
|
|
|
|
saved_state = state[channel]
|
|
|
|
|
state[channel] = State.thinking
|
|
|
|
|
time.sleep(1 * THROTTLE_FACTOR)
|
|
|
|
|
|
|
|
|
|
# First check if we should quit the current game
|
|
|
|
|
if saved_state != State.idle and msg == "!quit":
|
|
|
|
|
quit_game(channel)
|
|
|
|
|
elif saved_state == State.idle and msg == "startgame":
|
2018-09-19 17:04:38 +00:00
|
|
|
get_stories(channel, botnick)
|
2018-06-13 14:34:23 +00:00
|
|
|
elif saved_state == State.story_selection:
|
2018-09-19 17:04:38 +00:00
|
|
|
handle_story_selection(channel, msg, botnick)
|
2018-06-13 14:34:23 +00:00
|
|
|
elif saved_state == State.word_input:
|
|
|
|
|
handle_story_step(channel, msg)
|
|
|
|
|
else:
|
|
|
|
|
state[channel] = State.idle
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Handle how to quit the game
|
|
|
|
|
def quit_game(channel):
|
|
|
|
|
resetGlobals(channel)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "Ok, quitting the current game.")
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Handle user input when we are in story selection mode
|
2018-09-19 17:04:38 +00:00
|
|
|
def handle_story_selection(channel, msg, botnick):
|
2018-06-13 14:34:23 +00:00
|
|
|
global stories
|
|
|
|
|
global state
|
|
|
|
|
try:
|
|
|
|
|
imsg = int(msg)
|
2018-11-01 11:05:52 -04:00
|
|
|
if imsg < 0 or imsg > len(list(stories[channel])):
|
|
|
|
|
util.sendmsg(ircsock, channel, "Selection out of bounds. Try again!")
|
2018-06-13 14:34:23 +00:00
|
|
|
return
|
|
|
|
|
time.sleep(1 * THROTTLE_FACTOR)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock, channel, "Give me a second to load up {}".format(stories[channel][imsg][0])
|
2018-10-05 16:02:38 -04:00
|
|
|
)
|
2018-06-13 14:34:23 +00:00
|
|
|
|
|
|
|
|
with open(stories[channel][imsg][1], "r") as storyFile:
|
|
|
|
|
story[channel] = storyFile.read()
|
2018-10-05 16:02:38 -04:00
|
|
|
stories[channel] = {} # Clear out the saved selectable stories in memory
|
2018-09-19 17:04:38 +00:00
|
|
|
story_start(channel, botnick)
|
2018-06-13 14:34:23 +00:00
|
|
|
except ValueError:
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "Invalid selection. Try again!")
|
2018-06-13 14:34:23 +00:00
|
|
|
state[channel] = State.story_selection
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Handle when a story is being started
|
2018-09-19 17:04:38 +00:00
|
|
|
def story_start(channel, botnick):
|
2018-06-13 14:34:23 +00:00
|
|
|
global story
|
|
|
|
|
global state
|
|
|
|
|
global nextword
|
|
|
|
|
|
|
|
|
|
state[channel] = State.thinking
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock,
|
2018-10-05 16:02:38 -04:00
|
|
|
channel,
|
|
|
|
|
"Alright! Let's get started! Say '{}: <word>' to give me words.".format(
|
|
|
|
|
botnick
|
|
|
|
|
),
|
|
|
|
|
)
|
2018-06-13 14:34:23 +00:00
|
|
|
nextword[channel] = madlib.find_next_word(story[channel], True)
|
|
|
|
|
time.sleep(0.5 * THROTTLE_FACTOR)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "Give me {}:".format(nextword[channel][1]))
|
2018-06-13 14:34:23 +00:00
|
|
|
state[channel] = State.word_input
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Handle user input when we have asked the user for input and are expecting a
|
|
|
|
|
# response
|
|
|
|
|
def handle_story_step(channel, msg):
|
|
|
|
|
global state
|
|
|
|
|
global story
|
|
|
|
|
global nextword
|
|
|
|
|
|
|
|
|
|
state[channel] = State.thinking
|
2018-10-05 16:02:38 -04:00
|
|
|
word = nextword[channel] # madlib.find_next_word(story[channel])
|
2018-06-13 14:34:23 +00:00
|
|
|
if word is not None:
|
|
|
|
|
story[channel] = madlib.replace_word(story[channel], nextword[channel][0], msg)
|
|
|
|
|
time.sleep(1 * THROTTLE_FACTOR)
|
|
|
|
|
|
|
|
|
|
nextword[channel] = madlib.find_next_word(story[channel], True)
|
|
|
|
|
if nextword[channel] is None:
|
|
|
|
|
finish_story(channel)
|
|
|
|
|
return
|
2018-10-05 16:02:38 -04:00
|
|
|
# else
|
2018-06-13 14:34:23 +00:00
|
|
|
count = madlib.count_words(story[channel])
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock,
|
2018-10-05 16:02:38 -04:00
|
|
|
channel,
|
|
|
|
|
"Thanks! Now give me {} ({} words left)".format(nextword[channel][1], count),
|
|
|
|
|
)
|
2018-06-13 14:34:23 +00:00
|
|
|
state[channel] = State.word_input
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# Finish the story
|
|
|
|
|
def finish_story(channel):
|
|
|
|
|
global state
|
|
|
|
|
global story
|
|
|
|
|
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "Ok, here's the story...")
|
|
|
|
|
util.sendmsg(ircsock, channel, "=" * MAX_LINE)
|
2018-06-13 14:34:23 +00:00
|
|
|
for line in story[channel].splitlines():
|
|
|
|
|
for part in madlib.yield_lines(line, MAX_LINE):
|
2018-09-19 17:04:38 +00:00
|
|
|
time.sleep(0.6 * THROTTLE_FACTOR)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, part)
|
|
|
|
|
padlen = int((MAX_LINE - 9) / 2)
|
2018-06-13 14:34:23 +00:00
|
|
|
mod = (MAX_LINE - 9) % 2
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(ircsock, channel, "=" * padlen + " THE END " + "=" * (padlen + mod))
|
2018-06-13 14:34:23 +00:00
|
|
|
|
|
|
|
|
story[channel] = ""
|
|
|
|
|
state[channel] = State.idle
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
# System things
|
|
|
|
|
def joinchan(chan):
|
2018-09-19 17:04:38 +00:00
|
|
|
global state
|
|
|
|
|
state[chan] = State.idle
|
2018-06-13 14:34:23 +00:00
|
|
|
ircsock.send("JOIN {}\n".format(chan))
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
def rollcall(channel, botnick):
|
2018-09-19 17:04:38 +00:00
|
|
|
global state
|
|
|
|
|
if channel not in state:
|
|
|
|
|
state[channel] = State.idle
|
|
|
|
|
|
|
|
|
|
if state[channel] == State.idle:
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock,
|
2018-10-05 16:02:38 -04:00
|
|
|
channel,
|
|
|
|
|
"Do you like MadLibs? Start a collaborative story by saying '{}: startgame'".format(
|
|
|
|
|
botnick
|
|
|
|
|
),
|
|
|
|
|
)
|
2018-09-19 17:04:38 +00:00
|
|
|
else:
|
2018-11-01 11:05:52 -04:00
|
|
|
util.sendmsg(
|
|
|
|
|
ircsock,
|
2018-10-05 16:02:38 -04:00
|
|
|
channel,
|
|
|
|
|
"A game is already in progress. Say '{}: <word>' to provide me with the next word or '{}: !quit' to stop the current game".format(
|
|
|
|
|
botnick, botnick
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
|
|
|
|
|
def listen(botnick):
|
2018-10-05 16:02:38 -04:00
|
|
|
botmsgre = re.compile(
|
|
|
|
|
"^{}\:?\s*(.*)$".format(botnick)
|
|
|
|
|
) # re to strip the bot's name from a message
|
2018-06-13 14:34:23 +00:00
|
|
|
while 1:
|
2018-11-01 11:05:52 -04:00
|
|
|
ircmsg = ircsock.recv(2048).decode("utf-8")
|
2018-10-05 16:02:38 -04:00
|
|
|
ircmsg = ircmsg.strip("\n\r")
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-10-05 13:56:15 -04:00
|
|
|
if ircmsg[:4] == "PING":
|
2018-11-01 11:05:52 -04:00
|
|
|
util.ping(ircsock, ircmsg)
|
2018-06-13 14:34:23 +00:00
|
|
|
|
2018-10-09 02:50:06 -04:00
|
|
|
formatted = util.format_message(ircmsg)
|
2018-06-13 14:34:23 +00:00
|
|
|
if "" == formatted:
|
|
|
|
|
continue
|
|
|
|
|
# print formatted
|
|
|
|
|
|
|
|
|
|
split = formatted.split("\t")
|
|
|
|
|
msgtime = split[0]
|
|
|
|
|
user = split[1]
|
|
|
|
|
command = split[2]
|
|
|
|
|
channel = split[3]
|
|
|
|
|
message = split[4]
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
if (
|
|
|
|
|
message.startswith("!rollcall") == True
|
|
|
|
|
or message.startswith("!help") == True
|
|
|
|
|
):
|
2018-06-13 14:34:23 +00:00
|
|
|
rollcall(channel, botnick)
|
|
|
|
|
elif message.startswith(botnick) == True:
|
|
|
|
|
botmsg = botmsgre.match(message).group(1)
|
2018-09-19 17:04:38 +00:00
|
|
|
handle_bot_msg(channel, botmsg, botnick)
|
2018-06-13 14:34:23 +00:00
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
time.sleep(1 * THROTTLE_FACTOR)
|
|
|
|
|
|
2018-10-05 16:02:38 -04:00
|
|
|
|
2018-06-13 14:34:23 +00:00
|
|
|
ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
2018-11-01 11:05:52 -04:00
|
|
|
util.connect(ircsock, args)
|
|
|
|
|
listen(args.nick)
|