├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── bot_config.json ├── commands.py ├── logging.conf ├── markov_chain.py ├── math_parser.py └── twitch_irc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ehsan Kia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **SimpleTwitchBot** 2 | =============== 3 | 4 | This is a basic implementation of a Twitch IRC Bot coded in Python. 5 | It contains most of the gory specifics needed to interact with Twitch chat. 6 | I've included a couple basic commands as examples, but this is intended to just be a skeleton and not a fully featured bot. 7 | 8 | If you want something even more barebone than this, checkout [BareboneTwitchBot](https://github.com/EhsanKia/BareboneTwitchBot). 9 | 10 | # Installation and usage 11 | All you should need is Pyhton 2.7+ with [Twisted](https://twistedmatrix.com/trac/) installed. 12 | You then copy this project in a folder, configure the bot and run `twitch_irc.py`. 13 | 14 | #### Configuration: 15 | Make sure to modify the following values in `bot_config.json`: 16 | - `channel`: Twitch channel which the bot will run on 17 | - `username`: The bot's Twitch user 18 | - `oauth_key`: IRC oauth_key for the bot user (from [here](http://twitchapps.com/tmi/)) 19 | - `owner_list`: List of Twitch users which have admin powers on bot 20 | - `ignore_list`: List of Twitch users which will be ignored by the bot 21 | 22 | **Warning**: Make sure all channel and user names above are in lowercase. 23 | 24 | #### Usage: 25 | The main command-line window will show chat log and other extra messsages. 26 | 27 | You can enter commands by pressing CTRL+C on the command line: 28 | - `q`: Closes the bot 29 | - `r`: Reloads the code in `bot.py` and reconnects 30 | - `rm`: reloads the code in `markov_chain.py` 31 | - `ra`: reloads the code in `commands.py` and reloads commands 32 | - `p`: Pauses bot, ignoring commands from non-admins 33 | - `t `: Runs a test command with the bot's reply not being sent to the channel 34 | - `s `: Say something as the bot in the channel 35 | 36 | As you can see, the bot was made to be easy to modify live. 37 | You can simply modify most of the code and quickly reload it. 38 | The bot will also auto-reconnect if the connection is lost. 39 | 40 | # Code Overview 41 | 42 | #####`twitch_irc.py` 43 | This is the file that you run. It just starts up a Twisted IRC connection with the bot protocol. 44 | The bot is currently built to only run in one channel, but you can still open all the files over 45 | to another folder with a different config and run it in parallel. 46 | 47 | #####`bot.py` 48 | Contains the bot IRC protocol. The main guts of the bot are here. 49 | 50 | #####`commands.py` 51 | This is where the commands are stored. The code is built to be modular. 52 | Each "Command" class has: 53 | - `perm` variable from the Permission Enum to set access level 54 | - `__init__` function that initializes the command 55 | - `match` function that checks if this command needs to run 56 | - `run` function which actually runs the command 57 | - `close` function which is used to cleanup and save things 58 | 59 | All commands are passed the bot instance where they can get list of mods, subs and active users. 60 | `match` and `run` are also passed the name of the user issuing the command and the message. 61 | 62 | #####`markov_chain.py` 63 | A simple Markov Chain chat bot which learns from chat messages and tries to generate coherent replies. 64 | By default, it's only invoked with "!chat" or "!chat about " commands, but you can change the 65 | `chattiness` parameter in the file to a small fraction like 0.01 to have it randomly reply to people. 66 | It usually needs a couple hundred lines of chat to really start becoming effective. 67 | 68 | # Contact 69 | If you have any extra questions about the code, you can send me a PM on twitch: @ehsankia 70 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from twisted.words.protocols import irc 2 | from twisted.internet import reactor 3 | from collections import defaultdict 4 | from commands import Permission 5 | from threading import Thread 6 | import traceback 7 | import commands 8 | import requests 9 | import logging 10 | import signal 11 | import json 12 | import time 13 | 14 | USERLIST_API = "http://tmi.twitch.tv/group/user/{}/chatters" 15 | with open('bot_config.json') as fp: 16 | CONFIG = json.load(fp) 17 | 18 | 19 | class TwitchBot(irc.IRCClient, object): 20 | last_warning = defaultdict(int) 21 | owner_list = CONFIG['owner_list'] 22 | ignore_list = CONFIG['ignore_list'] 23 | nickname = str(CONFIG['username']) 24 | password = str(CONFIG['oauth_key']) 25 | channel = "#" + str(CONFIG['channel']) 26 | 27 | host_target = False 28 | pause = False 29 | commands = [] 30 | 31 | def signedOn(self): 32 | self.factory.wait_time = 1 33 | logging.warning("Signed on as {}".format(self.nickname)) 34 | 35 | signal.signal(signal.SIGINT, self.manual_action) 36 | 37 | # When first starting, get user list 38 | url = USERLIST_API.format(self.channel[1:]) 39 | data = requests.get(url).json() 40 | self.users = set(sum(data['chatters'].values(), [])) 41 | self.mods = set() 42 | self.subs = set() 43 | 44 | # Get data structures stored in factory 45 | self.activity = self.factory.activity 46 | self.tags = self.factory.tags 47 | 48 | # Load commands 49 | self.reload_commands() 50 | 51 | # Join channel 52 | self.sendLine("CAP REQ :twitch.tv/membership") 53 | self.sendLine("CAP REQ :twitch.tv/commands") 54 | self.sendLine("CAP REQ :twitch.tv/tags") 55 | self.join(self.channel) 56 | 57 | def joined(self, channel): 58 | logging.warning("Joined %s" % channel) 59 | 60 | def privmsg(self, user, channel, msg): 61 | # Extract twitch name 62 | name = user.split('!', 1)[0].lower() 63 | 64 | # Catch twitch specific commands 65 | if name in ["jtv", "twitchnotify"]: 66 | self.jtv_command(msg) 67 | return 68 | 69 | # Log the message 70 | logging.info("{}: {}".format(name, msg)) 71 | 72 | # Ignore messages by ignored user 73 | if name in self.ignore_list: 74 | return 75 | 76 | # Ignore message sent to wrong channel 77 | if channel != self.channel: 78 | return 79 | 80 | # Check if bot is paused 81 | if not self.pause or name in self.owner_list: 82 | self.process_command(name, msg) 83 | else: 84 | self.process_command("__USER__", "__UPDATE__") 85 | 86 | # Log user activity 87 | self.activity[name] = time.time() 88 | 89 | def modeChanged(self, user, channel, added, modes, args): 90 | if channel != self.channel: 91 | return 92 | 93 | # Keep mod list up to date 94 | func = 'add' if added else 'discard' 95 | for name in args: 96 | getattr(self.mods, func)(name) 97 | 98 | change = 'added' if added else 'removed' 99 | info_msg = "Mod {}: {}".format(change, ', '.join(args)) 100 | logging.warning(info_msg) 101 | 102 | def userJoined(self, user, channel): 103 | '''Update userlist when user joins''' 104 | if channel == self.channel: 105 | self.users.add(user) 106 | 107 | def userLeft(self, user, channel): 108 | '''Update userlist when user leaves''' 109 | if channel == self.channel: 110 | self.users.discard(user) 111 | 112 | def parsemsg(self, s): 113 | """Breaks a message from an IRC server into its prefix, command, and arguments.""" 114 | tags = {} 115 | prefix = '' 116 | trailing = [] 117 | if s[0] == '@': 118 | tags_str, s = s[1:].split(' ', 1) 119 | tag_list = tags_str.split(';') 120 | tags = dict(t.split('=') for t in tag_list) 121 | if s[0] == ':': 122 | prefix, s = s[1:].split(' ', 1) 123 | if s.find(' :') != -1: 124 | s, trailing = s.split(' :', 1) 125 | args = s.split() 126 | args.append(trailing) 127 | else: 128 | args = s.split() 129 | command = args.pop(0).lower() 130 | return tags, prefix, command, args 131 | 132 | def lineReceived(self, line): 133 | '''Parse IRC line''' 134 | 135 | # First, we check for any custom twitch commands 136 | tags, prefix, cmd, args = self.parsemsg(line) 137 | if cmd == "hosttarget": 138 | self.hostTarget(*args) 139 | elif cmd == "clearchat": 140 | self.clearChat(*args) 141 | elif cmd == "notice": 142 | self.notice(tags, args) 143 | elif cmd == "privmsg": 144 | self.userState(prefix, tags) 145 | 146 | # Remove tag information 147 | if line[0] == "@": 148 | line = line.split(' ', 1)[1] 149 | 150 | # Then we let IRCClient handle the rest 151 | super(TwitchBot, self).lineReceived(line) 152 | 153 | def hostTarget(self, channel, target): 154 | '''Track and update hosting status''' 155 | target = target.split(' ')[0] 156 | if target == "-": 157 | self.host_target = None 158 | logging.warning("Exited host mode") 159 | else: 160 | self.host_target = target 161 | logging.warning("Now hosting {}".format(target)) 162 | 163 | def clearChat(self, channel, target=None): 164 | '''Log chat clear notices''' 165 | if target: 166 | logging.warning("{} was timed out".format(target)) 167 | else: 168 | logging.warning("chat was cleared") 169 | 170 | def notice(self, tags, args): 171 | '''Log all chat mode changes''' 172 | if "msg-id" not in tags: 173 | return 174 | 175 | msg_id = tags['msg-id'] 176 | if msg_id == "subs_on": 177 | logging.warning("Subonly mode ON") 178 | elif msg_id == "subs_off": 179 | logging.warning("Subonly mode OFF") 180 | elif msg_id == "slow_on": 181 | logging.warning("Slow mode ON") 182 | elif msg_id == "slow_off": 183 | logging.warning("Slow mode OFF") 184 | elif msg_id == "r9k_on": 185 | logging.warning("R9K mode ON") 186 | elif msg_id == "r9k_off": 187 | logging.warning("R9K mode OFF") 188 | 189 | def userState(self, prefix, tags): 190 | '''Track user tags''' 191 | name = prefix.split("!")[0] 192 | self.tags[name].update(tags) 193 | 194 | if 'subscriber' in tags: 195 | if tags['subscriber'] == '1': 196 | self.subs.add(name) 197 | elif name in self.subs: 198 | self.subs.discard(name) 199 | 200 | if 'user-type' in tags: 201 | if tags['user-type'] == 'mod': 202 | self.mods.add(name) 203 | elif name in self.mods: 204 | self.mods.discard(name) 205 | 206 | def write(self, msg): 207 | '''Send message to channel and log it''' 208 | self.msg(self.channel, msg) 209 | logging.info("{}: {}".format(self.nickname, msg)) 210 | 211 | def get_permission(self, user): 212 | '''Returns the users permission level''' 213 | if user in self.owner_list: 214 | return Permission.Admin 215 | elif user in self.mods: 216 | return Permission.Moderator 217 | elif user in self.subs: 218 | return Permission.Subscriber 219 | return Permission.User 220 | 221 | def process_command(self, user, msg): 222 | perm_levels = ['User', 'Subscriber', 'Moderator', 'Owner'] 223 | perm = self.get_permission(user) 224 | msg = msg.strip() 225 | first = True 226 | 227 | # Flip through commands and execute the first one that matches 228 | # Check if user has permission to execute command 229 | # Also reduce warning message spam by limiting it to one per minute 230 | for cmd in self.commands: 231 | try: 232 | match = cmd.match(self, user, msg) 233 | if not match or not first: 234 | continue 235 | cname = cmd.__class__.__name__ 236 | if perm < cmd.perm: 237 | if time.time() - self.last_warning[cname] < 60: 238 | continue 239 | self.last_warning[cname] = time.time() 240 | reply = "{}: You don't have access to that command. Minimum level is {}." 241 | self.write(reply.format(user, perm_levels[cmd.perm])) 242 | else: 243 | cmd.run(self, user, msg) 244 | first = False 245 | except: 246 | logging.error(traceback.format_exc()) 247 | 248 | def manual_action(self, *args): 249 | cmd = raw_input("Command: ").strip() 250 | if cmd == "q": # Stop bot 251 | self.terminate() 252 | elif cmd == 'r': # Reload bot 253 | self.reload() 254 | elif cmd == 'rc': # Reload commands 255 | self.reload_commands() 256 | elif cmd == 'p': # Pause bot 257 | self.pause = not self.pause 258 | elif cmd == 'd': # try to enter debug mode 259 | IPythonThread(self).start() 260 | elif cmd.startswith("s"): 261 | # Say something as the bot 262 | self.write(cmd[2:]) 263 | 264 | def jtv_command(self, msg): 265 | if "subscribed" in msg: 266 | # Someone subscribed 267 | logging.warning(msg) 268 | 269 | reply = "Thanks for subbing!" 270 | if " just subscribed" in msg: 271 | user = msg.split(' just ')[0] 272 | reply = "{}: {}".format(user, reply) 273 | elif " subscribed for" in msg: 274 | user = msg.split(" subscribed for")[0] 275 | reply = "{}: {}".format(user, reply) 276 | self.write(reply) 277 | 278 | def get_active_users(self, t=60*10): 279 | ''' Returns list of users active in chat in 280 | the past t seconds (default: 10m)''' 281 | 282 | now = time.time() 283 | active_users = [] 284 | for user, last in self.activity.items(): 285 | if now - last < t: 286 | active_users.append(user) 287 | 288 | return active_users 289 | 290 | def close_commands(self): 291 | # Gracefully end commands 292 | for cmd in self.commands: 293 | try: 294 | cmd.close(self) 295 | except: 296 | logging.error(traceback.format_exc()) 297 | 298 | def reload_commands(self): 299 | logging.warning("Reloading commands") 300 | 301 | # Reload commands 302 | self.close_commands() 303 | cmds = reload(commands) 304 | self.commands = [ 305 | cmds.General(self), 306 | cmds.OwnerCommands(self), 307 | cmds.SimpleReply(self), 308 | cmds.Calculator(self), 309 | cmds.Timer(self), 310 | ] 311 | 312 | def reload(self): 313 | logging.warning("Reloading bot!") 314 | self.close_commands() 315 | self.quit() 316 | 317 | def terminate(self): 318 | self.close_commands() 319 | reactor.stop() 320 | 321 | 322 | class IPythonThread(Thread): 323 | def __init__(self, b): 324 | Thread.__init__(self) 325 | self.bot = b 326 | 327 | def run(self): 328 | logger = logging.getLogger() 329 | handler = logger.handlers[0] 330 | handler.setLevel(logging.ERROR) 331 | try: 332 | from IPython import embed 333 | bot = self.bot 334 | embed() 335 | del bot 336 | except ImportError: 337 | logging.error("IPython not installed, cannot debug.") 338 | handler.setLevel(logging.INFO) 339 | -------------------------------------------------------------------------------- /bot_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "channel": "ehsankia", 3 | "username": "ehsanbot", 4 | "oauth_key": "oauth:lolkswmebx7is0r894xzpy5lufwsl7", 5 | "ignore_list": ["nightbot", "moobot"], 6 | "owner_list": ["ehsankia"] 7 | } -------------------------------------------------------------------------------- /commands.py: -------------------------------------------------------------------------------- 1 | from math_parser import NumericStringParser 2 | from threading import Thread 3 | import time 4 | import re 5 | 6 | 7 | # Set of permissions for the commands 8 | class Permission: 9 | User, Subscriber, Moderator, Admin = range(4) 10 | 11 | 12 | # Base class for a command 13 | class Command(object): 14 | perm = Permission.Admin 15 | 16 | def __init__(self, bot): 17 | pass 18 | 19 | def match(self, bot, user, msg): 20 | return False 21 | 22 | def run(self, bot, user, msg): 23 | pass 24 | 25 | def close(self, bot): 26 | pass 27 | 28 | 29 | class MarkovLog(Command): 30 | '''Markov Chat bot that learns from chat 31 | and generates semi-sensible sentences 32 | 33 | You can use "!chat" to generate a completely 34 | random sentence, or "!chat about " to 35 | generate a sentence containing specific words''' 36 | 37 | perm = Permission.User 38 | 39 | def match(self, bot, user, msg): 40 | self.reply = bot.markov.log(msg) 41 | cmd = msg.lower() 42 | case1 = cmd.startswith("!chat about") 43 | case2 = cmd == "!chat" 44 | return case1 or case2 or self.reply 45 | 46 | def run(self, bot, user, msg): 47 | cmd = msg.lower() 48 | if cmd == "!chat": 49 | reply = bot.markov.random_chat() 50 | bot.write(reply) 51 | elif cmd.startswith("!chat about"): 52 | reply = bot.markov.chat(msg[12:]) 53 | bot.write(reply) 54 | else: 55 | bot.write(self.reply) 56 | 57 | 58 | class SimpleReply(Command): 59 | '''Simple meta-command to output a reply given 60 | a specific command. Basic key to value mapping.''' 61 | 62 | perm = Permission.User 63 | 64 | replies = { 65 | "!ping": "pong", 66 | "!headset": "Logitech G930 Headset", 67 | "!rts": "/me REAL TRAP SHIT", 68 | "!nfz": "/me NO FLEX ZONE!", 69 | } 70 | 71 | def match(self, bot, user, msg): 72 | cmd = msg.lower().strip() 73 | for key in self.replies: 74 | if cmd == key: 75 | return True 76 | return False 77 | 78 | def run(self, bot, user, msg): 79 | cmd = msg.lower().strip() 80 | 81 | for key, reply in self.replies.items(): 82 | if cmd == key: 83 | bot.write(reply) 84 | break 85 | 86 | 87 | class Calculator(Command): 88 | ''' A chat calculator that can do some pretty 89 | advanced stuff like sqrt and trigonometry 90 | 91 | Example: !calc log(5^2) + sin(pi/4)''' 92 | 93 | nsp = NumericStringParser() 94 | perm = Permission.User 95 | 96 | def match(self, bot, user, msg): 97 | return msg.lower().startswith("!calc ") 98 | 99 | def run(self, bot, user, msg): 100 | expr = msg.split(' ', 1)[1] 101 | try: 102 | result = self.nsp.eval(expr) 103 | if result.is_integer(): 104 | result = int(result) 105 | reply = "{} = {}".format(expr, result) 106 | bot.write(reply) 107 | except: 108 | bot.write("{} = ???".format(expr)) 109 | 110 | 111 | class General(Command): 112 | '''Some miscellaneous commands in here''' 113 | perm = Permission.User 114 | 115 | def match(self, bot, user, msg): 116 | cmd = msg.lower() 117 | if cmd.startswith("!active"): 118 | return True 119 | return False 120 | 121 | def run(self, bot, user, msg): 122 | reply = None 123 | cmd = msg.lower() 124 | 125 | if cmd.startswith("!active"): 126 | active = len(bot.get_active_users()) 127 | if active == 1: 128 | reply = "{}: There is {} active user in chat" 129 | else: 130 | reply = "{}: There are {} active users in chat" 131 | reply = reply.format(user, active) 132 | 133 | if reply: 134 | bot.write(reply) 135 | 136 | 137 | class Timer(Command): 138 | '''Sets a timer that will alert you when it runs out''' 139 | perm = Permission.Moderator 140 | 141 | def match(self, bot, user, msg): 142 | return msg.lower().startswith("!timer") 143 | 144 | def run(self, bot, user, msg): 145 | cmd = msg.lower() 146 | if cmd == "!timer": 147 | bot.write("Usage: !timer 30s or !timer 5m") 148 | return 149 | 150 | arg = cmd[7:].replace(' ', '') 151 | match = re.match("([\d\.]+)([sm]).*", arg) 152 | if match: 153 | d, u = match.groups() 154 | t = float(d) * (60 if u == 'm' else 1) 155 | thread = TimerThread(bot, user, t) 156 | thread.start() 157 | elif arg.isdigit(): 158 | thread = TimerThread(bot, user, int(arg) * 60) 159 | thread.start() 160 | else: 161 | bot.write("{}: Invalid argument".format(user)) 162 | 163 | 164 | class TimerThread(Thread): 165 | def __init__(self, b, u, t): 166 | Thread.__init__(self) 167 | self.bot = b 168 | self.user = u 169 | self.time = int(t) 170 | 171 | def run(self): 172 | secs = self.time % 60 173 | mins = self.time / 60 174 | 175 | msg = "{}: Timer started for".format(self.user) 176 | if mins > 0: 177 | msg += " {}m".format(mins) 178 | if secs > 0: 179 | msg += " {}s".format(secs) 180 | 181 | self.bot.write(msg) 182 | time.sleep(self.time) 183 | self.bot.write("{}: Time is up!".format(self.user)) 184 | 185 | 186 | class OwnerCommands(Command): 187 | '''Some miscellaneous commands for bot owners''' 188 | 189 | perm = Permission.Admin 190 | 191 | def match(self, bot, user, msg): 192 | cmd = msg.lower().replace(' ', '') 193 | if cmd.startswith("!sleep"): 194 | return True 195 | elif cmd.startswith("!wakeup"): 196 | return True 197 | 198 | return False 199 | 200 | def run(self, bot, user, msg): 201 | cmd = msg.lower().replace(' ', '') 202 | if cmd.startswith("!sleep"): 203 | bot.write("Going to sleep... bye!") 204 | bot.pause = True 205 | elif cmd.startswith("!wakeup"): 206 | bot.write("Good morning everyone!") 207 | bot.pause = False 208 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [logger_root] 5 | level=DEBUG 6 | handlers=console,file 7 | 8 | 9 | 10 | [handlers] 11 | keys=console,file 12 | 13 | [handler_console] 14 | class=StreamHandler 15 | level=INFO 16 | formatter=console 17 | args=(sys.stdout,) 18 | 19 | [handler_file] 20 | class=FileHandler 21 | level=DEBUG 22 | formatter=file 23 | args=('logs/bot.log',) 24 | 25 | 26 | 27 | [formatters] 28 | keys=console,file 29 | 30 | [formatter_file] 31 | format=[%(asctime)s] %(levelname)-8s | %(message)s 32 | datefmt=%m/%d/%Y %H:%M:%S 33 | 34 | [formatter_console] 35 | class=colorlog.ColoredFormatter 36 | format=[%(asctime)s] %(log_color)s%(message)s%(reset)s 37 | datefmt=%H:%M -------------------------------------------------------------------------------- /markov_chain.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | import cPickle as pickle 3 | import random 4 | import os 5 | 6 | class MarkovChat(object): 7 | chain_length = 2 8 | chattiness = 0 9 | max_words = 25 10 | messages_to_generate = 5 11 | separator = '\x01' 12 | stop_word = '\x02' 13 | ltable = defaultdict(list) 14 | rtable = defaultdict(list) 15 | train_data = 'logs/markov_train.txt' 16 | 17 | def __init__(self): 18 | self.load_file(self.train_data) 19 | 20 | def load_data(self): 21 | if os.path.exists(self.filename): 22 | with open(self.filename) as fp: 23 | self.ltable, self.rtable = pickle.load(fp) 24 | 25 | def save_data(self): 26 | with open(self.filename, 'w') as fp: 27 | obj = [self.ltable, self.rtable] 28 | pickle.dump(obj, fp) 29 | 30 | def split_message(self, message): 31 | # split the incoming message into words, i.e. ['what', 'up', 'bro'] 32 | words = message.split() 33 | 34 | # if the message is any shorter, it won't lead anywhere 35 | if len(words) > self.chain_length: 36 | 37 | # add some stop words onto the message 38 | # ['what', 'up', 'bro', '\x02'] 39 | words.append(self.stop_word) 40 | words = [self.stop_word, self.stop_word] + words 41 | 42 | # len(words) == 4, so range(4-2) == range(2) == 0, 1, meaning 43 | # we return the following slices: [0:3], [1:4] 44 | # or ['what', 'up', 'bro'], ['up', 'bro', '\x02'] 45 | for i in range(len(words) - self.chain_length): 46 | yield words[i:i + self.chain_length + 1] 47 | 48 | def generate_message(self, seed): 49 | gen_words = deque(seed) 50 | 51 | for i in range(self.max_words/2): 52 | lwords = gen_words[0], gen_words[1] 53 | rwords = gen_words[-2], gen_words[-1] 54 | lkey = self.separator.join(lwords).lower() 55 | rkey = self.separator.join(rwords).lower() 56 | oldlen = len(gen_words) 57 | 58 | if gen_words[0] != self.stop_word and lkey in self.ltable: 59 | next_word = random.choice(self.ltable[lkey]) 60 | gen_words.appendleft(next_word) 61 | 62 | if gen_words[-1] != self.stop_word and rkey in self.rtable: 63 | next_word = random.choice(self.rtable[rkey]) 64 | gen_words.append(next_word) 65 | 66 | if oldlen == len(gen_words): 67 | break 68 | 69 | return ' '.join(gen_words).strip('\x02 ') 70 | 71 | def log(self, msg): 72 | # speak only when spoken to, or when the spirit moves me 73 | if msg.startswith('!') or 'http://' in msg or not msg.count(' '): 74 | return 75 | 76 | with open(self.train_data, 'a') as fp: 77 | fp.write(msg + "\n") 78 | 79 | messages = [] 80 | if random.random() < self.chattiness: 81 | for words in self.split_message(msg): 82 | # if we should say something, generate some messages based on what 83 | # was just said and select the longest, then add it to the list 84 | best_message = '' 85 | for i in range(self.messages_to_generate): 86 | generated = self.generate_message(words) 87 | if len(generated) > len(best_message): 88 | best_message = generated 89 | 90 | if len(best_message.split()) > 3: 91 | messages.append(best_message) 92 | 93 | self.train(msg) 94 | 95 | if messages: 96 | return random.choice(messages) 97 | 98 | def train(self, msg): 99 | for words in self.split_message(msg): 100 | # grab everything but the last word 101 | lkey = self.separator.join(words[1:]).lower() 102 | rkey = self.separator.join(words[:-1]).lower() 103 | 104 | # add the last word to the set 105 | self.ltable[lkey].append(words[0]) 106 | self.rtable[rkey].append(words[-1]) 107 | 108 | def random_chat(self): 109 | key = random.choice(self.rtable.keys()) 110 | words = key.split(self.separator) 111 | return self.generate_message(words) 112 | 113 | def chat(self, context): 114 | words = context.split() 115 | if len(words) == 1: 116 | keys = [] 117 | word = words[0].lower() 118 | for k, v in self.rtable.items(): 119 | if k.endswith(word): 120 | keys.extend(v) 121 | if keys: 122 | k = random.choice(keys) 123 | words.append(k) 124 | if len(words) == 1: 125 | keys = [] 126 | word = words[0].lower() 127 | for k, v in self.ltable.items(): 128 | if k.startswith(word): 129 | keys.extend(v) 130 | if keys: 131 | k = random.choice(keys) 132 | words = [k] + words 133 | if len(words) < 2: 134 | return "Have nothing to say~" 135 | 136 | all_msgs = [] 137 | for ctx in zip(words, words[1:]): 138 | messages = [self.generate_message(ctx) for i in range(3)] 139 | all_msgs.extend(messages) 140 | 141 | ctx = context.lower() 142 | all_msgs = [m for m in all_msgs if m.lower() not in ctx] 143 | if not all_msgs: 144 | return "Have nothing to say~" 145 | return random.choice(all_msgs) 146 | 147 | def load_file(self, filename): 148 | try: 149 | with open(filename) as fp: 150 | lines = fp.readlines() 151 | except: 152 | lines = [] 153 | 154 | lines = list(set(lines)) 155 | for line in lines: 156 | self.train(line) 157 | -------------------------------------------------------------------------------- /math_parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import pyparsing as pyp 3 | import math 4 | import operator 5 | 6 | class NumericStringParser(object): 7 | ''' 8 | Most of this code comes from the fourFn.py pyparsing example 9 | http://pyparsing.wikispaces.com/file/view/fourFn.py 10 | http://pyparsing.wikispaces.com/message/view/home/15549426 11 | __author__='Paul McGuire' 12 | 13 | All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it 14 | more easily in other places. 15 | ''' 16 | def pushFirst(self, strg, loc, toks ): 17 | self.exprStack.append( toks[0] ) 18 | def pushUMinus(self, strg, loc, toks ): 19 | if toks and toks[0] == '-': 20 | self.exprStack.append( 'unary -' ) 21 | def __init__(self): 22 | """ 23 | expop :: '^' 24 | multop :: '*' | '/' 25 | addop :: '+' | '-' 26 | integer :: ['+' | '-'] '0'..'9'+ 27 | atom :: PI | E | real | fn '(' expr ')' | '(' expr ')' 28 | factor :: atom [ expop factor ]* 29 | term :: factor [ multop factor ]* 30 | expr :: term [ addop term ]* 31 | """ 32 | point = pyp.Literal( "." ) 33 | e = pyp.CaselessLiteral( "E" ) 34 | fnumber = pyp.Combine( pyp.Word( "+-"+pyp.nums, pyp.nums ) + 35 | pyp.Optional( point + pyp.Optional( pyp.Word( pyp.nums ) ) ) + 36 | pyp.Optional( e + pyp.Word( "+-"+pyp.nums, pyp.nums ) ) ) 37 | ident = pyp.Word(pyp.alphas, pyp.alphas+pyp.nums+"_$") 38 | plus = pyp.Literal( "+" ) 39 | minus = pyp.Literal( "-" ) 40 | mult = pyp.Literal( "*" ) 41 | div = pyp.Literal( "/" ) 42 | lpar = pyp.Literal( "(" ).suppress() 43 | rpar = pyp.Literal( ")" ).suppress() 44 | addop = plus | minus 45 | multop = mult | div 46 | expop = pyp.Literal( "^" ) 47 | pi = pyp.CaselessLiteral( "PI" ) 48 | expr = pyp.Forward() 49 | atom = ((pyp.Optional(pyp.oneOf("- +")) + 50 | (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst)) 51 | | pyp.Optional(pyp.oneOf("- +")) + pyp.Group(lpar+expr+rpar) 52 | ).setParseAction(self.pushUMinus) 53 | # by defining exponentiation as "atom [ ^ factor ]..." instead of 54 | # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right 55 | # that is, 2^3^2 = 2^(3^2), not (2^3)^2. 56 | factor = pyp.Forward() 57 | factor << atom + pyp.ZeroOrMore( ( expop + factor ).setParseAction( 58 | self.pushFirst ) ) 59 | term = factor + pyp.ZeroOrMore( ( multop + factor ).setParseAction( 60 | self.pushFirst ) ) 61 | expr << term + pyp.ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) ) 62 | self.bnf = expr 63 | # map operator symbols to corresponding arithmetic operations 64 | epsilon = 1e-12 65 | self.opn = { 66 | "+" : operator.add, 67 | "-" : operator.sub, 68 | "*" : operator.mul, 69 | "/" : operator.truediv, 70 | "^" : operator.pow 71 | } 72 | self.fn = { 73 | "abs" : abs, 74 | "trunc" : lambda a: int(a), 75 | "round" : round, 76 | "sgn" : lambda a: abs(a)>epsilon and cmp(a, 0) or 0 77 | } 78 | 79 | for n in dir(math): 80 | f = getattr(math, n) 81 | if callable(f): 82 | self.fn[n] = f 83 | 84 | self.exprStack = [] 85 | def evaluateStack(self, s ): 86 | op = s.pop() 87 | if op == 'unary -': 88 | return -self.evaluateStack( s ) 89 | if op in "+-*/^": 90 | op2 = self.evaluateStack( s ) 91 | op1 = self.evaluateStack( s ) 92 | return self.opn[op]( op1, op2 ) 93 | elif op == "PI": 94 | return math.pi # 3.1415926535 95 | elif op == "E": 96 | return math.e # 2.718281828 97 | elif op in self.fn: 98 | return self.fn[op]( self.evaluateStack( s ) ) 99 | elif op[0].isalpha(): 100 | return 0 101 | else: 102 | return float( op ) 103 | def eval(self, num_string, parseAll = True): 104 | self.exprStack = [] 105 | results = self.bnf.parseString(num_string, parseAll) 106 | val = self.evaluateStack( self.exprStack[:] ) 107 | return val -------------------------------------------------------------------------------- /twitch_irc.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import protocol, reactor 2 | from collections import defaultdict 3 | 4 | import bot 5 | import time 6 | import logging 7 | import logging.config 8 | logging.config.fileConfig('logging.conf') 9 | 10 | 11 | class BotFactory(protocol.ClientFactory): 12 | protocol = bot.TwitchBot 13 | 14 | tags = defaultdict(dict) 15 | activity = dict() 16 | wait_time = 1 17 | 18 | def clientConnectionLost(self, connector, reason): 19 | logging.error("Lost connection, reconnecting") 20 | self.protocol = reload(bot).TwitchBot 21 | connector.connect() 22 | 23 | def clientConnectionFailed(self, connector, reason): 24 | msg = "Could not connect, retrying in {}s" 25 | logging.warning(msg.format(self.wait_time)) 26 | time.sleep(self.wait_time) 27 | self.wait_time = min(512, self.wait_time * 2) 28 | connector.connect() 29 | 30 | 31 | if __name__ == "__main__": 32 | reactor.connectTCP('irc.twitch.tv', 6667, BotFactory()) 33 | reactor.run() 34 | --------------------------------------------------------------------------------