From 45029d95253b94c539c2ae1cfa30cf7994d791a7 Mon Sep 17 00:00:00 2001 From: mio Date: Fri, 14 Sep 2018 23:37:47 +0000 Subject: [PATCH] Add listener object --- irc.py | 90 ++++++++++++++++++++++++++++++++++++++++++-------------- ramen.py | 56 ++++++++++++----------------------- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/irc.py b/irc.py index 9e4037d..fd47f16 100644 --- a/irc.py +++ b/irc.py @@ -8,18 +8,23 @@ class IRC: debug = False - def run(self, handler, conf): + def run(self, listen_hook, conf): """A routine that connects to a server, joins channels, and initialises - the request handler in a loop.""" - self.connect(conf.server, conf.bot_nick) + the request listener in a loop.""" + # Conf settings needed by listen(), also used by disconnect() + self.bot_nick = conf.bot_nick + self.admin_user = conf.admin_user + self.admin_code = conf.admin_code + self.connect(conf.server, self.bot_nick) self.join_channels(conf.channels) while 1: sleep(2) - data = self.receive(debug=conf.debug) - self.keep_alive(data) - self.msg = self.parse_msg(data, conf.req_prefix) + data = self.receive() + self.keepalive(data) + self.msg = self.parse(data, conf.req_prefix) for c in conf.channels: - handler(self.msg, c) + # Pass in a context dict for handlers + listen_hook({"msg": self.msg, "listen_chan": c}) def connect(self, server, bot_nick): """Connect to the server and sends user/nick information.""" @@ -32,11 +37,25 @@ class IRC: bot_nick + " " + bot_nick) self.send("NICK", bot_nick) - def disconnect(self, resp_msg, quit_msg, admin_user): + def disconnect(self, resp_msg, quit_msg): """Notify the admin user and disconnect from the server.""" - self.send("PRIVMSG", resp_msg, recvr=admin_user) + self.send("PRIVMSG", resp_msg, recvr=self.admin_user) self.send("QUIT", ":" + quit_msg) + # Currently only one server per app instance is supported, so + # disconnect also exits the app + exit("Shutting down ...") + def keepalive(self, line): + """Stay connected to a server by responding to server pings.""" + if "PING" in line: + self.send("PONG", ":" + line.split(":", 2)[1].split(" ", 1)[0]) + + def join_channels(self, channels): + """Join channels given a list of channel names.""" + for c in channels: + if c.strip() != "" or c.strip() != "#": + self.send("JOIN", c) + def send(self, command, text, *args, **kwargs): """Send messages given the IRC command and text. Optionally specify a message recipient with `recvr=user`.""" @@ -54,25 +73,14 @@ class IRC: bs.decode("utf-8").strip() + "`") pass - def receive(self, *args, **kwargs): + def receive(self): """Get messages from the connected socket.""" data = self.sock.recv(2040).decode("utf-8").strip("\r\n") if self.debug: print("[debug][recv] " + data) return data - def keep_alive(self, line): - """Stay connected to a server by responding to server pings.""" - if "PING" in line: - self.send("PONG", ":" + line.split(":", 2)[1].split(" ", 1)[0]) - - def join_channels(self, channels): - """Join channels given a list of channel names.""" - for c in channels: - if c.strip() != "" or c.strip() != "#": - self.send("JOIN", c) - - def parse_msg(self, line, req_prefix): + def parse(self, line, req_prefix): """Using received data from a socket, extract the request, the nick and username of the requester, the channel where the request originated and return a dictionary of values.""" @@ -85,3 +93,41 @@ class IRC: data["nick"] = line.split("!~", 1)[0][1:] data["user"] = line.split("!~", 2)[1][0:].split("@", 1)[0] return data + + def listen(self, context, trigger, handler, *args, **kwargs): + """Listen for a trigger and call the handler. It takes a context + dictionary (to determine the channel and recipient), trigger string and + corresponding handler function. Optional flags (chan, query, admin) can + be used to specify whether a trigger is active in channels or by + /msg. e.g. `chan=False, query=True` to make a trigger query-only. By + default, it will listen in both channels and private messages.""" + in_chan = kwargs.get("chan", True) + in_query = kwargs.get("query", True) + in_admin = kwargs.get("admin", False) + # Admin requests are query/pm only + if in_admin: + in_chan = False + msg = context["msg"] + channel = context["listen_chan"] + # Responses are sent via pm to the user by default, while requests made + # in a channel are sent to the channel. While it's possible to override + # the recvr, it usually easier to enable a trigger in the same location + # where the response will be sent. + context["recvr"] = msg["user"] + + if msg["req"] == trigger: + # Respond only in the channel where the request was made + if in_chan and channel == msg["req_chan"]: + context["recvr"] = msg["req_chan"] + handler(context) + # Respond to query/pm + elif in_query and msg["req_chan"] == self.bot_nick: + handler(context) + # Respond only to the admin user + elif in_admin and msg["user"].lower() == \ + self.admin_user.lower() and self.admin_code in msg["req"]: + handler(context) + + def reply(self, cxt, text): + """Alias of send() with PRIVMSG command preset.""" + self.send("PRIVMSG", text, recvr=cxt["recvr"]) diff --git a/ramen.py b/ramen.py index febe403..6a9dd3e 100644 --- a/ramen.py +++ b/ramen.py @@ -1,4 +1,3 @@ -from sys import exit from random import randint import config as cfg @@ -9,39 +8,24 @@ class Ramen: """Requests with a ramen theme.""" def main(self): - """Instantiate an IRC object and pass in the request handler.""" + """Instantiate an IRC object and attach the listeners.""" self.irc = IRC() self.irc.debug = cfg.debug - self.irc.run(self.handle, cfg) + self.irc.run(self.add_listeners, cfg) - def handle(self, msg, channel): - """Listen for requests in a channel and match them to functions.""" - self.msg = msg - if self.msg["req_chan"] == cfg.bot_nick: - # Respond to some commands only from admin user - if self.msg["user"].lower() == cfg.admin_user.lower() and \ - cfg.admin_code in self.msg["req"]: - self.handle_admin_req(self.msg["req"], cfg.admin_user) + def add_listeners(self, cxt): + """Map triggers to handlers.""" + self.irc.listen(cxt, "exit " + cfg.admin_code, self.quit, admin=True) + self.irc.listen(cxt, "rollcall", self.rollcall) + self.irc.listen(cxt, "help", self.rollcall) + self.irc.listen(cxt, "water " + cfg.bot_nick, self.water) + self.irc.listen(cxt, "botsnack " + cfg.bot_nick, self.botsnack) - # Respond only in the channel the request was made - if channel == self.msg["req_chan"]: - # General commands - if self.msg["req"] == "rollcall" or self.msg["req"] == "help": - self.handle_rollcall() - elif self.msg["req"] == ("water " + cfg.bot_nick): - self.handle_water() - elif self.msg["req"] == ("botsnack " + cfg.bot_nick): - self.handle_botsnack() + def quit(self, cxt): + """Disconnect from the server and quit.""" + self.irc.disconnect("Okay, okay, I'll leave. (´・ω・`)", "noodling off") - def handle_admin_req(self, req, admin_user): - """Perform admin functions.""" - if "exit" in req: - self.irc.send("PRIVMSG", "Okay, okay, I'll leave. (´・ω・`)", \ - recvr=admin_user) - self.irc.send("QUIT", ":noodling off") - exit("Shutting down ...") - - def handle_rollcall(self): + def rollcall(self, cxt): resp = ( "一、二、三、らーめん缶! " "Hello, I am a ramen vending machine. " @@ -49,21 +33,19 @@ class Ramen: "!help " "Support: +81 012-700-1MIO どうぞめしあがれ。" ) - self.irc.send("PRIVMSG", resp, recvr=self.msg["req_chan"]) + self.irc.reply(cxt, resp) - def handle_water(self): + def water(self, cxt): resp = [ ("\x01ACTION happily pours the hot liquid into a bowl of noodles " - "and offers it to ") + self.msg["nick"] + "\x01", + "and offers it to ") + cxt["msg"]["nick"] + "\x01", "( ^_^)o自自o(^_^ )Cheers!", "Water Level [/////////] 200% - Thanks! (^▽^)" ] - self.irc.send("PRIVMSG", resp[randint(0, len(resp)-1)], \ - recvr=self.msg["req_chan"]) + self.irc.reply(cxt, resp[randint(0, len(resp)-1)]) - def handle_botsnack(self): - self.irc.send("PRIVMSG", "Ramen time anytime! ヽ(´▽`)/", \ - recvr=self.msg["req_chan"]) + def botsnack(self, cxt): + self.irc.reply(cxt, "Ramen time anytime! 自o(´▽`)/") app = Ramen()