Merge branch 'better-multilines' into 'master'

Better multilines by adding flood protection, consolidating privmsg/reply stuff

Closes #21 and #23

See merge request !14
This commit is contained in:
Brian S. Stephan 2017-02-12 12:04:37 -06:00
commit 5d5b58daa5
10 changed files with 101 additions and 76 deletions

View File

@ -143,7 +143,8 @@ class Acro(Plugin):
self.game.state = 1 self.game.state = 1
self.game.channel = channel self.game.channel = channel
self.bot.privmsg(self.game.channel, "starting a new game of acro. it will run until you tell it to quit.") self.bot.reply(None, "starting a new game of acro. it will run until you tell it to quit.",
explicit_target=self.game.channel)
self._start_new_round() self._start_new_round()
@ -156,8 +157,9 @@ class Acro(Plugin):
self.game.rounds[-1].acro = acro self.game.rounds[-1].acro = acro
sleep_time = self.game.rounds[-1].seconds_to_submit + (self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3)) sleep_time = self.game.rounds[-1].seconds_to_submit + (self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3))
self.bot.privmsg(self.game.channel, "the round has started! your acronym is '{0:s}'. " self.bot.reply(None, "the round has started! your acronym is '{0:s}'. "
"submit within {1:d} seconds via !acro submit [meaning]".format(acro, sleep_time)) "submit within {1:d} seconds via !acro submit [meaning]".format(acro, sleep_time),
explicit_target=self.game.channel)
t = threading.Thread(target=self.thread_do_process_submissions, args=(sleep_time,)) t = threading.Thread(target=self.thread_do_process_submissions, args=(sleep_time,))
t.daemon = True t.daemon = True
@ -258,7 +260,7 @@ class Acro(Plugin):
self.game.state = 3 self.game.state = 3
self.game.rounds[-1].sub_shuffle = list(self.game.rounds[-1].submissions.keys()) self.game.rounds[-1].sub_shuffle = list(self.game.rounds[-1].submissions.keys())
random.shuffle(self.game.rounds[-1].sub_shuffle) random.shuffle(self.game.rounds[-1].sub_shuffle)
self.bot.privmsg(self.game.channel, "here are the results. vote with !acro vote [number]") self.bot.reply(None, "here are the results. vote with !acro vote [number]", explicit_target=self.game.channel)
self._print_round_acros() self._print_round_acros()
t = threading.Thread(target=self.thread_do_process_votes, args=()) t = threading.Thread(target=self.thread_do_process_votes, args=())
@ -271,7 +273,8 @@ class Acro(Plugin):
i = 0 i = 0
for s in self.game.rounds[-1].sub_shuffle: for s in self.game.rounds[-1].sub_shuffle:
log.debug("%s is %s", str(i), s) log.debug("%s is %s", str(i), s)
self.bot.privmsg(self.game.channel, " {0:d}: {1:s}".format(i+1, self.game.rounds[-1].submissions[s])) self.bot.reply(None, " {0:d}: {1:s}".format(i+1, self.game.rounds[-1].submissions[s]),
explicit_target=self.game.channel)
i += 1 i += 1
def _take_acro_vote(self, nick, vote): def _take_acro_vote(self, nick, vote):
@ -295,7 +298,7 @@ class Acro(Plugin):
"""Clean up and output for ending the current round.""" """Clean up and output for ending the current round."""
self.game.state = 4 self.game.state = 4
self.bot.privmsg(self.game.channel, "voting's over! here are the scores for the round:") self.bot.reply(None, "voting's over! here are the scores for the round:", explicit_target=self.game.channel)
self._print_round_scores() self._print_round_scores()
self._add_round_scores_to_game_scores() self._add_round_scores_to_game_scores()
@ -309,7 +312,8 @@ class Acro(Plugin):
i = 0 i = 0
for s in list(self.game.rounds[-1].submissions.keys()): for s in list(self.game.rounds[-1].submissions.keys()):
votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s] votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s]
self.bot.privmsg(self.game.channel, " {0:d} ({1:s}): {2:d}".format(i+1, s, len(votes))) self.bot.reply(None, " {0:d} ({1:s}): {2:d}".format(i+1, s, len(votes)),
explicit_target=self.game.channel)
i += 1 i += 1
def _add_round_scores_to_game_scores(self): def _add_round_scores_to_game_scores(self):
@ -336,14 +340,14 @@ class Acro(Plugin):
self.game.state = 0 self.game.state = 0
self.game.quit = False self.game.quit = False
self.bot.privmsg(self.game.channel, "game's over! here are the final scores:") self.bot.reply(None, "game's over! here are the final scores:", explicit_target=self.game.channel)
self._print_game_scores() self._print_game_scores()
def _print_game_scores(self): def _print_game_scores(self):
"""Print the final calculated scores.""" """Print the final calculated scores."""
for s in list(self.game.scores.keys()): for s in list(self.game.scores.keys()):
self.bot.privmsg(self.game.channel, " {0:s}: {1:d}".format(s, self.game.scores[s])) self.bot.reply(None, " {0:s}: {1:d}".format(s, self.game.scores[s]), explicit_target=self.game.channel)
plugin = Acro plugin = Acro

View File

@ -0,0 +1 @@
"""Dispatch is Django/ircbot glue code that allows REST clients to hit Django views and execute IRC bot stuff."""

View File

@ -9,24 +9,23 @@ log = logging.getLogger('dispatch.models')
class Dispatcher(models.Model): class Dispatcher(models.Model):
"""Organize dispatchers by key.""" """Organize dispatchers by key."""
key = models.CharField(max_length=16, unique=True) key = models.CharField(max_length=16, unique=True)
class Meta: class Meta:
"""Meta options."""
permissions = ( permissions = (
('send_message', "Can send messages to dispatchers"), ('send_message', "Can send messages to dispatchers"),
) )
def __str__(self): def __str__(self):
"""String representation.""" """String representation."""
return "{0:s}".format(self.key) return "{0:s}".format(self.key)
class DispatcherAction(models.Model): class DispatcherAction(models.Model):
"""Handle requests to dispatchers and do something with them.""" """Handle requests to dispatchers and do something with them."""
PRIVMSG_TYPE = 'privmsg' PRIVMSG_TYPE = 'privmsg'
@ -44,5 +43,4 @@ class DispatcherAction(models.Model):
def __str__(self): def __str__(self):
"""String representation.""" """String representation."""
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination) return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination)

View File

@ -6,22 +6,29 @@ from dispatch.models import Dispatcher, DispatcherAction
class DispatcherActionSerializer(serializers.ModelSerializer): class DispatcherActionSerializer(serializers.ModelSerializer):
"""Serializer for the individual actions associated to a preconfigured dispatcher."""
class Meta: class Meta:
"""Meta options."""
model = DispatcherAction model = DispatcherAction
fields = ('id', 'dispatcher', 'type', 'destination') fields = ('id', 'dispatcher', 'type', 'destination')
class DispatcherSerializer(serializers.ModelSerializer): class DispatcherSerializer(serializers.ModelSerializer):
"""Serializer for a dispatcher, a set of actions for a key."""
actions = DispatcherActionSerializer(many=True, read_only=True) actions = DispatcherActionSerializer(many=True, read_only=True)
class Meta: class Meta:
"""Meta options."""
model = Dispatcher model = Dispatcher
fields = ('id', 'key', 'actions') fields = ('id', 'key', 'actions')
class DispatchMessageSerializer(serializers.Serializer): class DispatchMessageSerializer(serializers.Serializer):
"""Serializer for dispatch messaging."""
message = serializers.CharField() message = serializers.CharField()
status = serializers.CharField(read_only=True) status = serializers.CharField(read_only=True)

View File

@ -18,8 +18,10 @@ log = logging.getLogger('dispatch.views')
class HasSendMessagePermission(IsAuthenticated): class HasSendMessagePermission(IsAuthenticated):
"""Class to check if the authenticated user can send messages via dispatch."""
def has_permission(self, request, view): def has_permission(self, request, view):
"""Check user permission for dispatch.send_message."""
if request.user.has_perm('dispatch.send_message'): if request.user.has_perm('dispatch.send_message'):
return True return True
@ -27,7 +29,6 @@ class HasSendMessagePermission(IsAuthenticated):
class DispatcherList(generics.ListAPIView): class DispatcherList(generics.ListAPIView):
"""List all dispatchers.""" """List all dispatchers."""
queryset = Dispatcher.objects.all() queryset = Dispatcher.objects.all()
@ -35,7 +36,6 @@ class DispatcherList(generics.ListAPIView):
class DispatcherDetail(generics.RetrieveAPIView): class DispatcherDetail(generics.RetrieveAPIView):
"""Detail the given dispatcher.""" """Detail the given dispatcher."""
queryset = Dispatcher.objects.all() queryset = Dispatcher.objects.all()
@ -43,12 +43,12 @@ class DispatcherDetail(generics.RetrieveAPIView):
class DispatcherDetailByKey(DispatcherDetail): class DispatcherDetailByKey(DispatcherDetail):
"""Detail a specific dispatcher."""
lookup_field = 'key' lookup_field = 'key'
class DispatchMessage(generics.GenericAPIView): class DispatchMessage(generics.GenericAPIView):
"""Send a message to the given dispatcher.""" """Send a message to the given dispatcher."""
permission_classes = (HasSendMessagePermission,) permission_classes = (HasSendMessagePermission,)
@ -57,12 +57,13 @@ class DispatchMessage(generics.GenericAPIView):
serializer_class = DispatchMessageSerializer serializer_class = DispatchMessageSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
dispatcher = self.get_object() """Return a default message, since this needs POST."""
data = {'message': "", 'status': "READY"} data = {'message': "", 'status': "READY"}
message = self.serializer_class(data=data) message = self.serializer_class(data=data)
return Response(message.initial_data) return Response(message.initial_data)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Accept and dispatch a provided message."""
dispatcher = self.get_object() dispatcher = self.get_object()
message = self.serializer_class(data=request.data) message = self.serializer_class(data=request.data)
if message.is_valid(): if message.is_valid():
@ -77,9 +78,9 @@ class DispatchMessage(generics.GenericAPIView):
# connect over XML-RPC and send # connect over XML-RPC and send
try: try:
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT) bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT)
bot = xmlrpc.client.ServerProxy(bot_url) bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
log.debug("sending '%s' to channel %s", text, action.destination) log.debug("sending '%s' to channel %s", text, action.destination)
bot.privmsg(action.destination, text) bot.reply(None, text, False, action.destination)
except Exception as e: except Exception as e:
new_data = copy.deepcopy(message.data) new_data = copy.deepcopy(message.data)
new_data['status'] = "FAILED - {0:s}".format(str(e)) new_data['status'] = "FAILED - {0:s}".format(str(e))
@ -101,12 +102,12 @@ class DispatchMessage(generics.GenericAPIView):
class DispatchMessageByKey(DispatchMessage): class DispatchMessageByKey(DispatchMessage):
"""Dispatch a message for a specific key."""
lookup_field = 'key' lookup_field = 'key'
class DispatcherActionList(generics.ListAPIView): class DispatcherActionList(generics.ListAPIView):
"""List all dispatchers.""" """List all dispatchers."""
queryset = DispatcherAction.objects.all() queryset = DispatcherAction.objects.all()
@ -114,7 +115,6 @@ class DispatcherActionList(generics.ListAPIView):
class DispatcherActionDetail(generics.RetrieveAPIView): class DispatcherActionDetail(generics.RetrieveAPIView):
"""Detail the given dispatcher.""" """Detail the given dispatcher."""
queryset = DispatcherAction.objects.all() queryset = DispatcherAction.objects.all()

View File

@ -30,8 +30,8 @@ def send_privmsg(request):
message = form.cleaned_data['message'] message = form.cleaned_data['message']
bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT) bot_url = 'http://{0:s}:{1:d}/'.format(settings.IRCBOT_XMLRPC_HOST, settings.IRCBOT_XMLRPC_PORT)
bot = xmlrpc.client.ServerProxy(bot_url) bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
bot.privmsg(target, message) bot.reply(None, message, False, target)
form = PrivmsgForm() form = PrivmsgForm()
else: else:
form = PrivmsgForm() form = PrivmsgForm()

View File

@ -53,6 +53,11 @@ class LenientServerConnection(irc.client.ServerConnection):
buffer_class = irc.buffer.LenientDecodingLineBuffer buffer_class = irc.buffer.LenientDecodingLineBuffer
def _prep_message(self, string):
"""Override SimpleIRCClient._prep_message to add some logging."""
log.debug("preparing message %s", string)
return super(LenientServerConnection, self)._prep_message(string)
class DrReactor(irc.client.Reactor): class DrReactor(irc.client.Reactor):
"""Customize the basic IRC library's Reactor with more features.""" """Customize the basic IRC library's Reactor with more features."""
@ -318,6 +323,7 @@ class IRCBot(irc.client.SimpleIRCClient):
"""A single-server IRC bot class.""" """A single-server IRC bot class."""
reactor_class = DrReactor reactor_class = DrReactor
splitter = "..."
def __init__(self, reconnection_interval=60): def __init__(self, reconnection_interval=60):
super(IRCBot, self).__init__() super(IRCBot, self).__init__()
@ -371,7 +377,8 @@ class IRCBot(irc.client.SimpleIRCClient):
t.start() t.start()
# register XML-RPC stuff # register XML-RPC stuff
self.xmlrpc.register_function(self.privmsg, 'privmsg') self.xmlrpc_register_function(self.privmsg, 'privmsg')
self.xmlrpc_register_function(self.reply, 'reply')
def _connected_checker(self): def _connected_checker(self):
if not self.connection.is_connected(): if not self.connection.is_connected():
@ -832,35 +839,9 @@ class IRCBot(irc.client.SimpleIRCClient):
return return
log.debug("OUTGOING PRIVMSG: t[%s] m[%s]", target, text) log.debug("OUTGOING PRIVMSG: t[%s] m[%s]", target, text)
self.connection.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
splitter = "..." def reply(self, event, replystr, stop=False, explicit_target=None):
# split messages that are too long. Max length is 512.
# TODO: this does not properly handle when the nickmask has been masked by the ircd
# is the above still the case?
space = 512 - len('\r\n') - len(' PRIVMSG ') - len(target) - len(' :') - len(self.nickmask) - len(' :')
splitspace = space - (len(splitter) + 1)
if len(text) > space:
times = 1
while len(text) > splitspace:
splitpos = text.rfind(' ', 0, splitspace)
splittext = text[0:splitpos] + ' ' + splitter
text = splitter + ' ' + text[splitpos+1:]
self.connection.send_raw("PRIVMSG {0:s} :{1:s}".format(target, splittext))
times = times + 1
if times >= 4:
# this is stupidly long, abort
return
# done splitting
self.connection.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
else:
self.connection.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
def reply(self, event, replystr, stop=False):
"""Reply over IRC to replypath or return a string with the reply. """Reply over IRC to replypath or return a string with the reply.
The primary utility for this is to properly handle recursion. The The primary utility for this is to properly handle recursion. The
@ -873,27 +854,55 @@ class IRCBot(irc.client.SimpleIRCClient):
method will certainly have recursion do odd things with your module. method will certainly have recursion do odd things with your module.
Args: Args:
event incoming event event incoming event
replystr the message to reply with replystr the message to reply with
stop whether or not to let other handlers see this stop whether or not to let other handlers see this
explicit_target if event is none, use this for the destination
Returns: Returns:
The replystr if the event is inside recursion, or, potentially, The replystr if the event is inside recursion, or, potentially,
"NO MORE" to stop other event handlers from acting. "NO MORE" to stop other event handlers from acting.
""" """
log.debug("in reply for e[%s] r[%s]", event, replystr) if event:
replypath = ircbotlib.reply_destination_for_event(event) log.debug("in reply for e[%s] r[%s]", event, replystr)
replypath = ircbotlib.reply_destination_for_event(event)
recursing = getattr(event, '_recursing', False)
log.debug("determined recursing to be %s", recursing)
elif explicit_target:
log.debug("in reply for e[NO EVENT] r[%s]", replystr)
replypath = explicit_target
recursing = False
else:
log.warning("reply() called with no event and no explicit target, aborting")
return
log.debug("replypath: %s", replypath) log.debug("replypath: %s", replypath)
if replystr is not None: if replystr is not None:
recursing = getattr(event, '_recursing', False)
log.debug("determined recursing to be %s", recursing)
if recursing: if recursing:
return replystr return replystr
else: else:
lines = 0
replies = replystr.split('\n') replies = replystr.split('\n')
for reply in replies: for reply in replies:
# split messages that are too long. max length is 512, but we also need to
# account for display and that'll show our hostname
space = 512 - len(':{0:s} PRIVMSG {1:s} :\r\n'.format(self.nickmask, replypath))
splitspace = space - (len(self.splitter) + 1)
while len(reply) > space:
# do splitting
splitpos = reply.rfind(' ', 0, splitspace)
splittext = reply[0:splitpos] + ' ' + self.splitter
reply = self.splitter + ' ' + reply[splitpos + 1:]
self.privmsg(replypath, splittext)
# antiflood
lines += 1
time.sleep(int(0.5 * lines))
# done splitting, or it was never necessary
self.privmsg(replypath, reply) self.privmsg(replypath, reply)
if stop: if stop:
return "NO MORE" return "NO MORE"

View File

@ -2,7 +2,7 @@
import logging import logging
from django.forms import Form, CharField from django.forms import Form, CharField, Textarea
log = logging.getLogger('markov.forms') log = logging.getLogger('markov.forms')
@ -12,4 +12,4 @@ class PrivmsgForm(Form):
"""Accept a privmsg to send to the ircbot.""" """Accept a privmsg to send to the ircbot."""
target = CharField() target = CharField()
message = CharField() message = CharField(widget=Textarea)

View File

@ -133,8 +133,9 @@ class Storycraft(Plugin):
player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game) player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game)
# tell the control channel # tell the control channel
self.bot.privmsg(master_channel, "{0:s} created a game of storycraft - do '!storycraft game " self.bot.reply(None, "{0:s} created a game of storycraft - do '!storycraft game "
"{1:d} join' to take part!".format(nick, game.pk)) "{1:d} join' to take part!".format(nick, game.pk),
explicit_target=master_channel)
log.debug("%s added a new game", nick) log.debug("%s added a new game", nick)
return self.bot.reply(event, "Game #{0:d} has been created. When everyone has joined, do " return self.bot.reply(event, "Game #{0:d} has been created. When everyone has joined, do "
@ -163,7 +164,8 @@ class Storycraft(Plugin):
# output results # output results
master_channel = settings.STORYCRAFT_MASTER_CHANNEL master_channel = settings.STORYCRAFT_MASTER_CHANNEL
self.bot.privmsg(master_channel, "{0:s} joined storycraft #{1:d}!".format(nick, game_id)) self.bot.reply(None, "{0:s} joined storycraft #{1:d}!".format(nick, game_id),
explicit_target=master_channel)
log.debug("%s joined game #%d", nick, game_id) log.debug("%s joined game #%d", nick, game_id)
return self.bot.reply(event, "{0:s}, welcome to the game.".format(nick)) return self.bot.reply(event, "{0:s}, welcome to the game.".format(nick))
else: else:
@ -235,9 +237,10 @@ class Storycraft(Plugin):
master_channel = settings.STORYCRAFT_MASTER_CHANNEL master_channel = settings.STORYCRAFT_MASTER_CHANNEL
# tell the control channel # tell the control channel
self.bot.privmsg(master_channel, "{0:s} started storycraft #{1:d}! - first player " self.bot.reply(None, "{0:s} started storycraft #{1:d}! - first player "
"is {2:s}, do '!storycraft game {1:d} show line' when the game is " "is {2:s}, do '!storycraft game {1:d} show line' when the game is "
"assigned to you.".format(nick, game_id, player.nick)) "assigned to you.".format(nick, game_id, player.nick),
explicit_target=master_channel)
log.debug("%s started game #%d", nick, game_id) log.debug("%s started game #%d", nick, game_id)
return self.bot.reply(event, "Game #{0:d} started.".format(game_id)) return self.bot.reply(event, "Game #{0:d} started.".format(game_id))
@ -343,22 +346,25 @@ class Storycraft(Plugin):
return_msg = 'Line logged. Please add another. ' + progress_str return_msg = 'Line logged. Please add another. ' + progress_str
else: else:
# notify the new owner, too # notify the new owner, too
self.bot.privmsg(player.nick, "You have a new line in storycraft " self.bot.reply(None, "You have a new line in storycraft "
"#{0:d}: '{1:s}' {2:s}" "#{0:d}: '{1:s}' {2:s}"
"".format(game_id, last_line.line, progress_str)) "".format(game_id, last_line.line, progress_str),
explicit_target=player.nick)
master_channel = settings.STORYCRAFT_MASTER_CHANNEL master_channel = settings.STORYCRAFT_MASTER_CHANNEL
log.debug("%s added a line to #%d", nick, game_id) log.debug("%s added a line to #%d", nick, game_id)
# log output # log output
if game.status == StorycraftGame.STATUS_IN_PROGRESS: if game.status == StorycraftGame.STATUS_IN_PROGRESS:
self.bot.privmsg(master_channel, "{0:s} added a line to storycraft " self.bot.reply(None, "{0:s} added a line to storycraft "
"#{1:d}! - next player is {2:s}" "#{1:d}! - next player is {2:s}"
"".format(nick, game_id, player.nick)) "".format(nick, game_id, player.nick),
explicit_target=master_channel)
return self.bot.reply(event, return_msg) return self.bot.reply(event, return_msg)
else: else:
self.bot.privmsg(master_channel, "{0:s} finished storycraft #{1:d}!" self.bot.reply(None, "{0:s} finished storycraft #{1:d}!"
"".format(nick, game_id)) "".format(nick, game_id),
explicit_target=master_channel)
return self.bot.reply(event, "Line logged (and game completed).") return self.bot.reply(event, "Line logged (and game completed).")
else: else:
return self.bot.reply(event, "Failed to assign game to next player.") return self.bot.reply(event, "Failed to assign game to next player.")

View File

@ -276,7 +276,7 @@ class Twitter(Plugin):
mentions.reverse() mentions.reverse()
for mention in mentions: for mention in mentions:
reply = self._return_tweet_or_retweet_text(tweet=mention, print_source=True) reply = self._return_tweet_or_retweet_text(tweet=mention, print_source=True)
self.bot.privmsg(out_chan, reply) self.bot.reply(None, reply, explicit_target=out_chan)
since_id = mention['id'] if mention['id'] > since_id else since_id since_id = mention['id'] if mention['id'] > since_id else since_id
twittersettings.mentions_since_id = since_id twittersettings.mentions_since_id = since_id