Skip to content

Instantly share code, notes, and snippets.

@adituv
Created May 26, 2016 20:12
Show Gist options
  • Select an option

  • Save adituv/5139d1489abd830df7942c3fb99a8baa to your computer and use it in GitHub Desktop.

Select an option

Save adituv/5139d1489abd830df7942c3fb99a8baa to your computer and use it in GitHub Desktop.
Twitch chatbot prototype with Sopel
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