Created
May 26, 2016 20:12
-
-
Save adituv/5139d1489abd830df7942c3fb99a8baa to your computer and use it in GitHub Desktop.
Twitch chatbot prototype with Sopel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from sopel.module import commands, rule, rate | |
| import queue | |
| import threading | |
| import time | |
| ############################################################################### | |
| ## SECTION: chat commands ## | |
| ############################################################################### | |
| @commands('help') | |
| @rate(0,0,3) | |
| def help(bot, trigger): | |
| """Custom help command. | |
| If called without an argument, gives a list of all commands | |
| provided by the bot. If called with an argument, gives information | |
| on that command. | |
| """ | |
| commands=getCommands() | |
| if trigger.group(2) is None: | |
| keys = list(commands.keys()) | |
| keys.sort() | |
| cmdNames = [] | |
| for cmd in keys: | |
| cmdNames.append(cmd) | |
| message = ", ".join(cmdNames) | |
| message = "Available commands: " + message + ". Type !help <command>" | |
| message = message + " for help on a specific command." | |
| bot.say(message) | |
| elif trigger.group(2) in commands: | |
| bot.reply("!{0}: {1}".format(trigger.group(2), | |
| commands[trigger.group(2)])) | |
| else: | |
| bot.reply("!{0}: unknown command.".format(trigger.group(2))) | |
| def getCommands(): | |
| """Gets a dict of all bot commands in the current module. | |
| Detects them by looking for all names in globals() decorated by | |
| @sopel.module.commands. The dictionary returned has as the key | |
| the command name, and as the value, the function called by that | |
| command. | |
| If a function has multiple associated command strings, it will | |
| occur in the result once for each command string. | |
| """ | |
| knownCommands = getattr(getCommands, "knownCommands", None) | |
| if knownCommands is None: | |
| cmdGlobals = [v for v in globals().values() if hasattr(v,"commands")] | |
| getCommands.knownCommands = {} | |
| for v in cmdGlobals: | |
| for cmd in v.commands: | |
| doc = v.__doc__ or "No help text given" | |
| # Allow overriding the doc string for !mod | |
| if hasattr(v,"__fakehelp__"): | |
| doc = v.__fakehelp__ | |
| getCommands.knownCommands[cmd] = doc | |
| knownCommands = getCommands.knownCommands | |
| return knownCommands | |
| @commands('songlist') | |
| @rate(0,0,10) | |
| def songlist(bot, trigger): | |
| """Gives information on available songs.""" | |
| bot.say("Coming soon! Setlists available: all GH games except Aerosmith; " | |
| "Reklist; Memelist; GH3 DLC") | |
| @commands('mod') | |
| def modresponse(bot, trigger): | |
| """Troll function. Claims to give mod, but times out the calling | |
| user. | |
| """ | |
| global timeouts | |
| timeouts.put( (trigger.nick, 10) ) | |
| modresponse.__fakehelp__ = ("If there aren't currently enough mods in chat, " | |
| "assigns mod on request.") | |
| ############################################################################### | |
| ## SECTION: regex-triggered chat rules ## | |
| ############################################################################### | |
| @rule('.*i like one.*') | |
| @rate(0,0,10) | |
| def oneresponse(bot, trigger): | |
| bot.say("Which one do you like?") | |
| @rule("(overdrive|cocaine|innovator|are you ready|adrenaline is pumping?" | |
| "|atomic|blockbuster|call me a leader|don't you try it|kille?r? machine|" | |
| "there's no fate|take control|.*brain ?power.*|.*O-oooooooooo.*)") | |
| def brainpower(bot, trigger): | |
| """Automatically times out any non-mod who posts any of the lyrics | |
| to Brain Power by NOMA. There are a few possible false-positives, | |
| such as "Overdrive is Rock Band's equivalent to Star Power" but | |
| these should be rather unlikely for now. | |
| """ | |
| global timeouts | |
| if not hasattr(brainpower, "lastSpeech"): | |
| brainpower.lastSpeech = 0 | |
| if brainpower.lastSpeech + 30 < time.time(): | |
| bot.say("No to brainpower. Just no.") | |
| brainpower.last_speech = time.time() | |
| timeouts.put( (trigger.nick, 10) ) | |
| ############################################################################### | |
| ## SECTION: Bot utilities ## | |
| ############################################################################### | |
| # A queue of timeouts to process. This is used rather than immediately | |
| # sending a timeout message to dynamically limit the rate to avoid | |
| # being global-banned for flooding a channel. | |
| timeouts = None | |
| # The consumer thread for the timeouts queue. Checks the conditions | |
| # managed by the rate manager thread for whether it can time out and | |
| # acts upon them, sending a timeout only if appropriate. | |
| timeoutThread = None | |
| # The thread to handle the time-based decay of the timeouts condition. | |
| # Can use any algorithm that guarantees fewer than 100 messages are | |
| # sent to a channel in 30 seconds, for a Twitch irc bot that has mod | |
| # status. | |
| rateManagerThread = None | |
| timeoutCount = 0 | |
| timeoutMutex = None # Mutex for timeoutCount. | |
| def rateManager(): | |
| """Timeout rate manager thread. | |
| Decrements the condition variable every 0.35 seconds, such that up | |
| to 85 timeouts will be permitted every second, while still not | |
| restricting a faster rate of timeouts when further from that quota. | |
| """ | |
| global timeoutMutex | |
| global timeoutCount | |
| while True: | |
| time.sleep(0.35) # 30 / 0.35 ~= 85.7 | |
| timeoutMutex.acquire() | |
| if timeoutCount > 0: | |
| timeoutCount = timeoutCount - 1 | |
| timeoutMutex.release() | |
| def runTimeout(bot): | |
| global timeoutMutex | |
| global timeoutCount | |
| while True: | |
| # This doesn't need to be a "while" as the sleep delay is | |
| # sufficient to guarantee the intended effect of waiting for | |
| # enough time to avoid a global ban. | |
| if timeoutCount > 85: | |
| time.sleep(0.5) | |
| user, timeout = timeouts.get() # NB. get() is a blocking call | |
| bot.say(".timeout " + user + " " + str(timeout), "#adituv") | |
| timeoutMutex.acquire() | |
| timeoutCount = timeoutCount + 1 | |
| timeoutMutex.release() | |
| def setup(bot): | |
| global timeouts, timeoutThread, rateManagerThread, timeoutMutex | |
| timeoutMutex = threading.Lock() | |
| timeouts = queue.Queue() | |
| bot.cap_req("AdituBot","twitch.tv/membership") | |
| rateManagerThread = | |
| threading.Thread(target=rateManager, daemon=True) | |
| timeoutThread = | |
| threading.Thread(target=runTimeout, args=(bot,), daemon=True) | |
| rateManagerThread.start() | |
| timeoutThread.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment