diff --git a/acro/ircplugin.py b/acro/ircplugin.py index 814cba8..07114d2 100644 --- a/acro/ircplugin.py +++ b/acro/ircplugin.py @@ -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 diff --git a/dispatch/__init__.py b/dispatch/__init__.py index e69de29..e8fe2e6 100644 --- a/dispatch/__init__.py +++ b/dispatch/__init__.py @@ -0,0 +1 @@ +"""Dispatch is Django/ircbot glue code that allows REST clients to hit Django views and execute IRC bot stuff.""" diff --git a/dispatch/models.py b/dispatch/models.py index eddb2a3..fb3ab48 100644 --- a/dispatch/models.py +++ b/dispatch/models.py @@ -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) diff --git a/dispatch/serializers.py b/dispatch/serializers.py index 500aa9d..ca38c16 100644 --- a/dispatch/serializers.py +++ b/dispatch/serializers.py @@ -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) diff --git a/dispatch/views.py b/dispatch/views.py index dc12ef0..21f534f 100644 --- a/dispatch/views.py +++ b/dispatch/views.py @@ -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() diff --git a/ircbot/admin.py b/ircbot/admin.py index d2ef34a..11c02fc 100644 --- a/ircbot/admin.py +++ b/ircbot/admin.py @@ -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() diff --git a/ircbot/bot.py b/ircbot/bot.py index 57516c6..e01b82a 100644 --- a/ircbot/bot.py +++ b/ircbot/bot.py @@ -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" diff --git a/ircbot/forms.py b/ircbot/forms.py index f413b74..ec89820 100644 --- a/ircbot/forms.py +++ b/ircbot/forms.py @@ -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) diff --git a/storycraft/ircplugin.py b/storycraft/ircplugin.py index d743fa7..d0509af 100644 --- a/storycraft/ircplugin.py +++ b/storycraft/ircplugin.py @@ -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.") diff --git a/twitter/ircplugin.py b/twitter/ircplugin.py index 2655190..04c247e 100644 --- a/twitter/ircplugin.py +++ b/twitter/ircplugin.py @@ -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