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.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()
@ -156,8 +157,9 @@ class Acro(Plugin):
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))
self.bot.privmsg(self.game.channel, "the round has started! your acronym is '{0:s}'. "
"submit within {1:d} seconds via !acro submit [meaning]".format(acro, sleep_time))
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),
explicit_target=self.game.channel)
t = threading.Thread(target=self.thread_do_process_submissions, args=(sleep_time,))
t.daemon = True
@ -258,7 +260,7 @@ class Acro(Plugin):
self.game.state = 3
self.game.rounds[-1].sub_shuffle = list(self.game.rounds[-1].submissions.keys())
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()
t = threading.Thread(target=self.thread_do_process_votes, args=())
@ -271,7 +273,8 @@ class Acro(Plugin):
i = 0
for s in self.game.rounds[-1].sub_shuffle:
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
def _take_acro_vote(self, nick, vote):
@ -295,7 +298,7 @@ class Acro(Plugin):
"""Clean up and output for ending the current round."""
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._add_round_scores_to_game_scores()
@ -309,7 +312,8 @@ class Acro(Plugin):
i = 0
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]
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
def _add_round_scores_to_game_scores(self):
@ -336,14 +340,14 @@ class Acro(Plugin):
self.game.state = 0
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()
def _print_game_scores(self):
"""Print the final calculated scores."""
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

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):
"""Organize dispatchers by key."""
key = models.CharField(max_length=16, unique=True)
class Meta:
"""Meta options."""
permissions = (
('send_message', "Can send messages to dispatchers"),
)
def __str__(self):
"""String representation."""
return "{0:s}".format(self.key)
class DispatcherAction(models.Model):
"""Handle requests to dispatchers and do something with them."""
PRIVMSG_TYPE = 'privmsg'
@ -44,5 +43,4 @@ class DispatcherAction(models.Model):
def __str__(self):
"""String representation."""
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):
"""Serializer for the individual actions associated to a preconfigured dispatcher."""
class Meta:
"""Meta options."""
model = DispatcherAction
fields = ('id', 'dispatcher', 'type', 'destination')
class DispatcherSerializer(serializers.ModelSerializer):
"""Serializer for a dispatcher, a set of actions for a key."""
actions = DispatcherActionSerializer(many=True, read_only=True)
class Meta:
"""Meta options."""
model = Dispatcher
fields = ('id', 'key', 'actions')
class DispatchMessageSerializer(serializers.Serializer):
"""Serializer for dispatch messaging."""
message = serializers.CharField()
status = serializers.CharField(read_only=True)

View File

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

View File

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

View File

@ -53,6 +53,11 @@ class LenientServerConnection(irc.client.ServerConnection):
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):
"""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."""
reactor_class = DrReactor
splitter = "..."
def __init__(self, reconnection_interval=60):
super(IRCBot, self).__init__()
@ -371,7 +377,8 @@ class IRCBot(irc.client.SimpleIRCClient):
t.start()
# 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):
if not self.connection.is_connected():
@ -832,35 +839,9 @@ class IRCBot(irc.client.SimpleIRCClient):
return
log.debug("OUTGOING PRIVMSG: t[%s] m[%s]", target, text)
self.connection.send_raw("PRIVMSG {0:s} :{1:s}".format(target, text))
splitter = "..."
# 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):
def reply(self, event, replystr, stop=False, explicit_target=None):
"""Reply over IRC to replypath or return a string with the reply.
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.
Args:
event incoming event
replystr the message to reply with
stop whether or not to let other handlers see this
event incoming event
replystr the message to reply with
stop whether or not to let other handlers see this
explicit_target if event is none, use this for the destination
Returns:
The replystr if the event is inside recursion, or, potentially,
"NO MORE" to stop other event handlers from acting.
"""
log.debug("in reply for e[%s] r[%s]", event, replystr)
replypath = ircbotlib.reply_destination_for_event(event)
if 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)
if replystr is not None:
recursing = getattr(event, '_recursing', False)
log.debug("determined recursing to be %s", recursing)
if recursing:
return replystr
else:
lines = 0
replies = replystr.split('\n')
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)
if stop:
return "NO MORE"

View File

@ -2,7 +2,7 @@
import logging
from django.forms import Form, CharField
from django.forms import Form, CharField, Textarea
log = logging.getLogger('markov.forms')
@ -12,4 +12,4 @@ class PrivmsgForm(Form):
"""Accept a privmsg to send to the ircbot."""
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)
# tell the control channel
self.bot.privmsg(master_channel, "{0:s} created a game of storycraft - do '!storycraft game "
"{1:d} join' to take part!".format(nick, game.pk))
self.bot.reply(None, "{0:s} created a game of storycraft - do '!storycraft game "
"{1:d} join' to take part!".format(nick, game.pk),
explicit_target=master_channel)
log.debug("%s added a new game", nick)
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
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)
return self.bot.reply(event, "{0:s}, welcome to the game.".format(nick))
else:
@ -235,9 +237,10 @@ class Storycraft(Plugin):
master_channel = settings.STORYCRAFT_MASTER_CHANNEL
# tell the control channel
self.bot.privmsg(master_channel, "{0:s} started storycraft #{1:d}! - first player "
"is {2:s}, do '!storycraft game {1:d} show line' when the game is "
"assigned to you.".format(nick, game_id, player.nick))
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 "
"assigned to you.".format(nick, game_id, player.nick),
explicit_target=master_channel)
log.debug("%s started game #%d", nick, 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
else:
# notify the new owner, too
self.bot.privmsg(player.nick, "You have a new line in storycraft "
"#{0:d}: '{1:s}' {2:s}"
"".format(game_id, last_line.line, progress_str))
self.bot.reply(None, "You have a new line in storycraft "
"#{0:d}: '{1:s}' {2:s}"
"".format(game_id, last_line.line, progress_str),
explicit_target=player.nick)
master_channel = settings.STORYCRAFT_MASTER_CHANNEL
log.debug("%s added a line to #%d", nick, game_id)
# log output
if game.status == StorycraftGame.STATUS_IN_PROGRESS:
self.bot.privmsg(master_channel, "{0:s} added a line to storycraft "
"#{1:d}! - next player is {2:s}"
"".format(nick, game_id, player.nick))
self.bot.reply(None, "{0:s} added a line to storycraft "
"#{1:d}! - next player is {2:s}"
"".format(nick, game_id, player.nick),
explicit_target=master_channel)
return self.bot.reply(event, return_msg)
else:
self.bot.privmsg(master_channel, "{0:s} finished storycraft #{1:d}!"
"".format(nick, game_id))
self.bot.reply(None, "{0:s} finished storycraft #{1:d}!"
"".format(nick, game_id),
explicit_target=master_channel)
return self.bot.reply(event, "Line logged (and game completed).")
else:
return self.bot.reply(event, "Failed to assign game to next player.")

View File

@ -276,7 +276,7 @@ class Twitter(Plugin):
mentions.reverse()
for mention in mentions:
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
twittersettings.mentions_since_id = since_id