From 265277b8dbdef8ce47d12c73b5547878a11d0513 Mon Sep 17 00:00:00 2001 From: aewens Date: Thu, 6 Sep 2018 23:31:13 -0400 Subject: [PATCH 01/10] Moved python core into own file, starting scheme version --- .gitignore | 1 + bot.scm | 22 ++++++++ bot/__init__.py | 114 +---------------------------------------- bot/core.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 113 deletions(-) create mode 100644 bot.scm create mode 100644 bot/core.py diff --git a/.gitignore b/.gitignore index 35ff8b2..2aaf706 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # App-specific settings.json +irc/ # ---> Python # Byte-compiled / optimized / DLL files diff --git a/bot.scm b/bot.scm new file mode 100644 index 0000000..5a4a7d2 --- /dev/null +++ b/bot.scm @@ -0,0 +1,22 @@ +(use tcp irregex ports srfi-13 srfi-14 srfi-69) + +(define bot-nick "BabiliBot|scm") + +(define (send out message) + (format out (string-append message "\r\n"))) + +(let-values + ([(in out) + (tcp-connect "127.0.0.1" 6667)]) + (send out + (string-append + "USER " + (string-join (vector->list (make-vector 4 bot-nick)) " "))) + (send out (string-append "NICK " bot-nick)) + (let loop + ([line (read-line in)]) + (if (eof-object? line) + line + (begin + (print line) + (loop (read-line in)))))) \ No newline at end of file diff --git a/bot/__init__.py b/bot/__init__.py index cd656e5..3d7ce55 100755 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1,113 +1 @@ -import json -import socket - -class Bot: - def __init__(self, server, port, botnick, channels): - self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.server = server - self.port = port - self.botnick = botnick - self.channels = channels - self.running = True - self.users = dict() - - self.recv_size = 2048 - - def send(self, message, *args): - response = message.format(*args) + "\n" - print("DEBUG: ", response) - self.ircsock.send(response.encode()) - - def send_message(self, target, message, *args): - msg = message.format(*args) - response = "PRIVMSG {0} :{1}".format(target, msg) + "\n" - print("DEBUG: ", response) - self.ircsock.send(response.encode()) - - def join(self, chan, confirmed=False): - self.send("JOIN {}", chan) - - message = "" - magic_string = "End of /NAMES list." - while magic_string not in message: - message = self.ircsock.recv(self.recv_size).decode() - message = message.strip("\n\r") - print(message) - - user_list = "= {} :".format(chan) - raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ") - prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u - users = list(filter(prefix_filter, raw_users)) - for user in users: - if user not in self.users: - self.users[user] = dict() - - def ping(self, message): - response = message.split("PING :")[1] - self.send("PONG :{0}", response) - - def parse(self, message): - before, after = message.split("PRIVMSG ", 1) - name = before.split("!", 1)[0][1:] - source, response = after.split(" :", 1) - return name, source, response - - def track_rename(self, message): - # :aewens|otg!aewensotg@tilde.team NICK aewens|otg2 - before, new_name = message.split("NICK ", 1) - name = before.split("!", 1)[0][1:] - user = self.users[name] - del self.users[name] - self.users[new_name] = user - - def load_settings(self, location): - with open(location, "r") as f: - self.settings = json.loads(f.read()) - - def start(self, callback, settings): - message = "" - registered = False - confirmed = True - - self.ircsock.connect((self.server, self.port)) - self.send("USER {0} {0} {0} {0}", self.botnick) - self.send("NICK {0}", self.botnick) - - self.load_settings(settings) - - password = self.settings["password"] or "" - confirm = self.settings["confirm"] or "" - email = self.settings["email"] or "" - - magic_string = "MODE {} +r".format(self.botnick) - while magic_string not in message: - message = self.ircsock.recv(self.recv_size).decode() - message = message.strip("\n\r") - print(message) - if not registered and "Password accepted" in message: - registered = True - if not registered and "choose a different nick" in message: - self.send_message("NickServ", "IDENTIFY {}", password) - if not confirmed and "Your account will expire" in message: - self.send_message("NickServ", "CONFIRM {}", self.confirm) - confirmed = True - - print("DEBUG: Joining") - - for channel in self.channels: - self.join(channel) - - print("DEBUG: Joined") - - while self.running: - message = self.ircsock.recv(self.recv_size).decode("UTF-8") - message = message.strip("\n\r") - print(message) - - if "PING :" in message: - self.ping(message) - elif "NICK " in message: - self.track_rename(message) - elif "PRIVMSG" in message: - name, source, response = self.parse(message) - callback(name, source, response) \ No newline at end of file +from bot.core import Bot \ No newline at end of file diff --git a/bot/core.py b/bot/core.py new file mode 100644 index 0000000..6bf5706 --- /dev/null +++ b/bot/core.py @@ -0,0 +1,132 @@ +import re +import json +import socket + +class Bot: + def __init__(self, server, port, botnick, channels): + self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server = server + self.port = port + self.botnick = botnick + self.channels = channels + self.running = True + + self.users = dict() + self.kickers = list() + + self.recv_size = 2048 + + def send(self, message, *args): + response = message.format(*args) + "\n" + print("DEBUG: ", response) + self.ircsock.send(response.encode()) + + def send_message(self, target, message, *args): + msg = message.format(*args) + response = "PRIVMSG {0} :{1}".format(target, msg) + "\n" + print("DEBUG: ", response) + self.ircsock.send(response.encode()) + + def join(self, chan): + self.send("JOIN {}", chan) + + message = "" + magic_string = "End of /NAMES list." + while magic_string not in message: + message = self.ircsock.recv(self.recv_size).decode() + message = message.strip("\n\r") + print(message) + + user_list = "= {} :".format(chan) + raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ") + prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u + users = list(filter(prefix_filter, raw_users)) + for user in users: + if user not in self.users: + self.users[user] = dict() + + def ping(self, message): + response = message.split("PING :")[1] + self.send("PONG :{0}", response) + + def parse(self, message): + before, after = message.split("PRIVMSG ", 1) + name = before.split("!", 1)[0][1:] + source, response = after.split(" :", 1) + return name, source, response + + def track_rename(self, message): + before, new_name = message.split("NICK ", 1) + name = before.split("!", 1)[0][1:] + user = self.users[name] + del self.users[name] + self.users[new_name] = user + + def answer_invite(self, message): + before, after = message.split("INVITE ", 1) + channel = after.split(":", 1)[1] + self.join(channel) + + def log_kick(self, message): + # :aewens!aewens@rightful.heir.to.chaos KICK #insane BabiliBot|py :aewens + regex = "KICK #\S+ {} :".format(self.botnick) + before, kicker = re.split(regex, message) + self.kickers.append(kicker) + + def load_settings(self, location): + with open(location, "r") as f: + self.settings = json.loads(f.read()) + + def stop(self): + self.running = False + self.send("QUIT") + + def start(self, callback, settings): + message = "" + registered = False + confirmed = True + + self.ircsock.connect((self.server, self.port)) + self.send("USER {0} {0} {0} {0}", self.botnick) + self.send("NICK {0}", self.botnick) + + self.load_settings(settings) + + password = self.settings["password"] or "" + confirm = self.settings["confirm"] or "" + email = self.settings["email"] or "" + + magic_string = "MODE {} +r".format(self.botnick) + while magic_string not in message: + message = self.ircsock.recv(self.recv_size).decode() + message = message.strip("\n\r") + print(message) + if not registered and "Password accepted" in message: + registered = True + if not registered and "choose a different nick" in message: + self.send_message("NickServ", "IDENTIFY {}", password) + if not confirmed and "Your account will expire" in message: + self.send_message("NickServ", "CONFIRM {}", self.confirm) + confirmed = True + + print("DEBUG: Joining") + + for channel in self.channels: + self.join(channel) + + print("DEBUG: Joined") + + while self.running: + message = self.ircsock.recv(self.recv_size).decode() + message = message.strip("\n\r") + print(message) + + if "PING :" in message: + self.ping(message) + elif "NICK " in message: + self.track_rename(message) + elif "INVITE " in message: + self.answer_invite(message) + elif "PRIVMSG" in message: + name, source, response = self.parse(message) + callback(name, source, response) \ No newline at end of file From 0cc70c0ee76b5074978d94d11c31be37cb6636c0 Mon Sep 17 00:00:00 2001 From: aewens Date: Mon, 10 Sep 2018 11:12:27 -0400 Subject: [PATCH 02/10] Added usermode +B for tildeverse RFC conformity --- bot/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/core.py b/bot/core.py index 6bf5706..93ea0e8 100644 --- a/bot/core.py +++ b/bot/core.py @@ -109,6 +109,8 @@ class Bot: self.send_message("NickServ", "CONFIRM {}", self.confirm) confirmed = True + self.send("MODE {} +B".format(self.botnick)) + print("DEBUG: Joining") for channel in self.channels: From 39ca8287ea1bf91f9880d468722a9dc25a04961b Mon Sep 17 00:00:00 2001 From: aewens Date: Mon, 10 Sep 2018 11:18:59 -0400 Subject: [PATCH 03/10] Added usermode +B to conform to tildeverse bot RFC --- bot.scm | 22 ---------------------- bot/core.py | 1 - 2 files changed, 23 deletions(-) delete mode 100644 bot.scm diff --git a/bot.scm b/bot.scm deleted file mode 100644 index 5a4a7d2..0000000 --- a/bot.scm +++ /dev/null @@ -1,22 +0,0 @@ -(use tcp irregex ports srfi-13 srfi-14 srfi-69) - -(define bot-nick "BabiliBot|scm") - -(define (send out message) - (format out (string-append message "\r\n"))) - -(let-values - ([(in out) - (tcp-connect "127.0.0.1" 6667)]) - (send out - (string-append - "USER " - (string-join (vector->list (make-vector 4 bot-nick)) " "))) - (send out (string-append "NICK " bot-nick)) - (let loop - ([line (read-line in)]) - (if (eof-object? line) - line - (begin - (print line) - (loop (read-line in)))))) \ No newline at end of file diff --git a/bot/core.py b/bot/core.py index 93ea0e8..8eb15f6 100644 --- a/bot/core.py +++ b/bot/core.py @@ -68,7 +68,6 @@ class Bot: self.join(channel) def log_kick(self, message): - # :aewens!aewens@rightful.heir.to.chaos KICK #insane BabiliBot|py :aewens regex = "KICK #\S+ {} :".format(self.botnick) before, kicker = re.split(regex, message) self.kickers.append(kicker) From 31e4d6da67f54892a30e2d2630f37c9f0d87fddb Mon Sep 17 00:00:00 2001 From: aewens Date: Mon, 10 Sep 2018 18:32:21 -0400 Subject: [PATCH 04/10] Setup basic framework layout, added more IRC sensor functions --- app.py | 19 ++++++++-- bot/core.py | 100 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/app.py b/app.py index 203cad7..4adf535 100755 --- a/app.py +++ b/app.py @@ -4,9 +4,22 @@ from bot import Bot bot = Bot("127.0.0.1", 6667, "BabiliBot|py", ["#bots"]) -def processor(name, source, response): +def handle_pm(name, response): + print("PM: {} - {}".format(name, response)) + +def handle_message(name, source, response): #bot.send_message(source, "Got response") - print(name, source, response) + print("MSG: {} {} - {}".format(name, source, response)) + +def handle_mode(channel, mode): + if mode == "-r": + bot.send_message("ChanServ", "REGISTER {}", channel) + bot.send_message("ChanServ", "SET Successor {} {}", chan, bot.botnick) + bot.send_message("ChanServ", "SET Founder {} {}", chan, bot.author) if __name__ == "__main__": - bot.start(processor, "settings.json") \ No newline at end of file + bot.start("settings.json", { + "pm": handle_pm, + "message": handle_message, + "mode": handle_mode + }) \ No newline at end of file diff --git a/bot/core.py b/bot/core.py index 8eb15f6..eafbff6 100644 --- a/bot/core.py +++ b/bot/core.py @@ -13,8 +13,13 @@ class Bot: self.users = dict() self.kickers = list() + self.invites = list() + + self.author = "" + self.kingme = [] self.recv_size = 2048 + self.splitter = "\n\r" def send(self, message, *args): response = message.format(*args) + "\n" @@ -34,9 +39,12 @@ class Bot: magic_string = "End of /NAMES list." while magic_string not in message: message = self.ircsock.recv(self.recv_size).decode() - message = message.strip("\n\r") + message = message.strip(self.splitter) print(message) + if chan in self.kingme: + self.try_to_king_me(chan) + user_list = "= {} :".format(chan) raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ") prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u @@ -49,38 +57,69 @@ class Bot: response = message.split("PING :")[1] self.send("PONG :{0}", response) + def get_name(self, text): + return text.split("!", 1)[0][1:] + def parse(self, message): before, after = message.split("PRIVMSG ", 1) - name = before.split("!", 1)[0][1:] + name = self.get_name(before) source, response = after.split(" :", 1) return name, source, response - def track_rename(self, message): + def handle_mode(self, message): + before, after = message.split("MODE ", 1) + name = self.get_name(before) + channel, mode = after.split(" ")[:2] + return channel, mode + + def handle_rename(self, message): before, new_name = message.split("NICK ", 1) - name = before.split("!", 1)[0][1:] + name = self.get_name(before) user = self.users[name] del self.users[name] self.users[new_name] = user + return user, new_name - def answer_invite(self, message): + def handle_invite(self, message): before, after = message.split("INVITE ", 1) + name = self.get_name(before) channel = after.split(":", 1)[1] self.join(channel) + self.invites.append({ + "name": name, + "channel": channel + }) + return channel, name - def log_kick(self, message): + def handle_kick(self, message): regex = "KICK #\S+ {} :".format(self.botnick) before, kicker = re.split(regex, message) self.kickers.append(kicker) + return kicker + + def try_to_king_me(self, chan): + self.send_message("ChanServ", "REGISTER {}", chan) + self.send_message("ChanServ", "SET Successor {} {}", chan, self.botnick) + self.send_message("ChanServ", "SET Founder {} {}", chan, self.author) def load_settings(self, location): with open(location, "r") as f: self.settings = json.loads(f.read()) + set_vars = [ + "author", + "kingme" + ] + + for name, attr in self.settings.items(): + if name in set_vars: + setattr(self, name, attr) + def stop(self): self.running = False self.send("QUIT") - def start(self, callback, settings): + def start(self, settings, callback): message = "" registered = False confirmed = True @@ -95,16 +134,22 @@ class Bot: confirm = self.settings["confirm"] or "" email = self.settings["email"] or "" + magic_phrase = { + "has_registered": "Password", + "needs_to_register": "choose a different nick", + "needs_to_confirm": "Your account will expire" + } + magic_string = "MODE {} +r".format(self.botnick) while magic_string not in message: message = self.ircsock.recv(self.recv_size).decode() - message = message.strip("\n\r") + message = message.strip(self.splitter) print(message) - if not registered and "Password accepted" in message: + if not registered and magic_phrase["has_registered"] in message: registered = True - if not registered and "choose a different nick" in message: + if not registered and magic_phrase["needs_to_register"] in message: self.send_message("NickServ", "IDENTIFY {}", password) - if not confirmed and "Your account will expire" in message: + if not confirmed and magic_phrase["needs_to_confirm"] in message: self.send_message("NickServ", "CONFIRM {}", self.confirm) confirmed = True @@ -119,15 +164,38 @@ class Bot: while self.running: message = self.ircsock.recv(self.recv_size).decode() - message = message.strip("\n\r") + message = message.strip(self.splitter) print(message) + if "raw" in callback: + callback["raw"](message) + if "PING :" in message: self.ping(message) + if "ping" in callback: + callback["ping"]() + elif "MODE " in message: + channel, mode = self.handle_mode(message) + if "mode" in callback: + callback["mode"](channel, mode) elif "NICK " in message: - self.track_rename(message) + old_name, new_name = self.handle_rename(message) + if "nick" in callback: + callback["nick"](old_name, new_name) + elif "KICK " in message: + kicker = self.handle_kick(message) + if "kick" in callback: + callback["kick"](kicker) elif "INVITE " in message: - self.answer_invite(message) - elif "PRIVMSG" in message: + channel, name = self.handle_invite(message) + if "invite" in callback: + callback["invite"](channel, name) + elif "PRIVMSG " in message: name, source, response = self.parse(message) - callback(name, source, response) \ No newline at end of file + if source == self.botnick and "pm" in callback: + callback["pm"](name, response) + elif "message" in callback: + callback["message"](name, source, response) + else: + if "unhandled" in callback: + callback["unhandled"](message) From 27ac672d6decf194fb1b2e6965cf3c0b00ca1e37 Mon Sep 17 00:00:00 2001 From: aewens Date: Tue, 11 Sep 2018 21:08:45 -0400 Subject: [PATCH 05/10] Added responses system, added actions system to react to response triggers --- actions/__init__.py | 14 ++++++++++++ actions/botlist.py | 12 ++++++++++ app.py | 53 ++++++++++++++++++++++++++++++++++++--------- bot/__init__.py | 4 +++- bot/core.py | 30 +++++-------------------- bot/responses.py | 30 +++++++++++++++++++++++++ bot/tasks.py | 3 +++ 7 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 actions/__init__.py create mode 100644 actions/botlist.py create mode 100644 bot/responses.py create mode 100644 bot/tasks.py diff --git a/actions/__init__.py b/actions/__init__.py new file mode 100644 index 0000000..3fed4d6 --- /dev/null +++ b/actions/__init__.py @@ -0,0 +1,14 @@ +from actions.botlist import botlist + +actions = [ + { + "type": "response", + "pattern": "!botlist", + "callback": botlist + }, + { + "type": "response", + "pattern": "!rollcall", + "callback": botlist + } +] \ No newline at end of file diff --git a/actions/botlist.py b/actions/botlist.py new file mode 100644 index 0000000..91ff332 --- /dev/null +++ b/actions/botlist.py @@ -0,0 +1,12 @@ +def botlist(self, name, source, response): + name = self.bot.botnick + author = self.bot.author + email = self.bot.settings["email"] + about = "the meta chat bot" + commands = ", ".join([ + "!botlist", + "!rollcall" + ]) + args = (name, author, email, about, commands) + message = "{} | {} <{}> | {} | {}".format(*args) + self.bot.send_message(source, message) \ No newline at end of file diff --git a/app.py b/app.py index 4adf535..23355c7 100755 --- a/app.py +++ b/app.py @@ -1,25 +1,58 @@ #!/usr/bin/env python3 -from bot import Bot +from bot import Bot, Tasks, Responses +from actions import actions + +kickers = list() +inviters = dict() +kingme = [ + "#chaos" +] bot = Bot("127.0.0.1", 6667, "BabiliBot|py", ["#bots"]) +responses = Responses(bot) + +for action in actions: + if "type" in action and "pattern" in action and "callback" in action: + responses.add_trigger( + action["type"], + action["pattern"], + action["callback"] + ) + +def try_to_king_me(bot, channel): + bot.send_message("ChanServ", "REGISTER {}", channel) + bot.send_message("ChanServ", "SET Successor {} {}", channel, bot.botnick) + bot.send_message("ChanServ", "SET Founder {} {}", channel, bot.author) def handle_pm(name, response): print("PM: {} - {}".format(name, response)) -def handle_message(name, source, response): - #bot.send_message(source, "Got response") - print("MSG: {} {} - {}".format(name, source, response)) - def handle_mode(channel, mode): if mode == "-r": - bot.send_message("ChanServ", "REGISTER {}", channel) - bot.send_message("ChanServ", "SET Successor {} {}", chan, bot.botnick) - bot.send_message("ChanServ", "SET Founder {} {}", chan, bot.author) + try_to_king_me(bot, channel) + +def handle_invite(channel, name): + if channel in kingme: + try_to_king_me(bot, channel) + + invites.append({ + "channel": channel, + "name": name + }) + +def handle_kick(kicker): + kickers.append(kicker) + +def handle_message(name, source, response): + print("MSG: {} {} - {}".format(name, source, response)) + responses.parse(name, source, response) if __name__ == "__main__": bot.start("settings.json", { "pm": handle_pm, - "message": handle_message, - "mode": handle_mode + "mode": handle_mode, + "invite": handle_invite, + "kick": handle_kick, + "message": handle_message }) \ No newline at end of file diff --git a/bot/__init__.py b/bot/__init__.py index 3d7ce55..eabec22 100755 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1 +1,3 @@ -from bot.core import Bot \ No newline at end of file +from bot.core import Bot +from bot.tasks import Tasks +from bot.responses import Responses \ No newline at end of file diff --git a/bot/core.py b/bot/core.py index eafbff6..13141b6 100644 --- a/bot/core.py +++ b/bot/core.py @@ -11,12 +11,9 @@ class Bot: self.channels = channels self.running = True + self.settings = dict() self.users = dict() - self.kickers = list() - self.invites = list() - self.author = "" - self.kingme = [] self.recv_size = 2048 self.splitter = "\n\r" @@ -42,9 +39,6 @@ class Bot: message = message.strip(self.splitter) print(message) - if chan in self.kingme: - self.try_to_king_me(chan) - user_list = "= {} :".format(chan) raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ") prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u @@ -85,32 +79,21 @@ class Bot: name = self.get_name(before) channel = after.split(":", 1)[1] self.join(channel) - self.invites.append({ - "name": name, - "channel": channel - }) return channel, name def handle_kick(self, message): regex = "KICK #\S+ {} :".format(self.botnick) before, kicker = re.split(regex, message) - self.kickers.append(kicker) return kicker - def try_to_king_me(self, chan): - self.send_message("ChanServ", "REGISTER {}", chan) - self.send_message("ChanServ", "SET Successor {} {}", chan, self.botnick) - self.send_message("ChanServ", "SET Founder {} {}", chan, self.author) - def load_settings(self, location): + set_vars = [ + "author" + ] + with open(location, "r") as f: self.settings = json.loads(f.read()) - set_vars = [ - "author", - "kingme" - ] - for name, attr in self.settings.items(): if name in set_vars: setattr(self, name, attr) @@ -196,6 +179,5 @@ class Bot: callback["pm"](name, response) elif "message" in callback: callback["message"](name, source, response) - else: - if "unhandled" in callback: + elif "unhandled" in callback: callback["unhandled"](message) diff --git a/bot/responses.py b/bot/responses.py new file mode 100644 index 0000000..39dbad6 --- /dev/null +++ b/bot/responses.py @@ -0,0 +1,30 @@ +import re + +class Responses: + def __init__(self, bot): + self.bot = bot + self.triggers = { + "name": dict(), + "source": dict(), + "response": dict() + } + + def add_trigger(self, trigger_type, pattern, callback): + if trigger_type in self.triggers: + self.triggers[trigger_type][pattern] = callback + + def parse(self, name, source, response): + check = { + "name": name, + "source": source, + "response": response + } + + for trigger in list(self.triggers.keys()): + for pattern, callback in self.triggers[trigger].items(): + if pattern[0] == "!" and pattern == response: + callback(self, name, source, response) + else: + regex = re.compile(pattern) + if regex.match(check[trigger]) is not None: + callback(self, name, source, response) \ No newline at end of file diff --git a/bot/tasks.py b/bot/tasks.py new file mode 100644 index 0000000..71c56e8 --- /dev/null +++ b/bot/tasks.py @@ -0,0 +1,3 @@ +class Tasks: + def __init__(self): + pass \ No newline at end of file From 626c0c48833ac75d1d597da9c8f0a9ba9d167de6 Mon Sep 17 00:00:00 2001 From: aewens Date: Thu, 13 Sep 2018 15:37:32 -0400 Subject: [PATCH 06/10] Added year and name into LICENSE, added in most of babili's actions --- .gitignore | 1 + LICENSE | 2 +- actions/__init__.py | 44 ++++++++++++++++++++++++++ actions/access.py | 37 ++++++++++++++++++++++ actions/botlist.py | 9 ++++-- actions/control.py | 27 ++++++++++++++++ actions/nomad.py | 0 actions/stupid.py | 58 ++++++++++++++++++++++++++++++++++ actions/summon.py | 25 +++++++++++++++ app.py | 40 +++++++++++++++--------- bot/core.py | 76 +++++++++++++++++++++++++++++++++++++++------ bot/responses.py | 43 ++++++++++++++++++++++--- data/.gitkeep | 0 13 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 actions/access.py create mode 100644 actions/control.py create mode 100644 actions/nomad.py create mode 100644 actions/stupid.py create mode 100644 actions/summon.py create mode 100644 data/.gitkeep diff --git a/.gitignore b/.gitignore index 2aaf706..d8b6c30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # App-specific settings.json +data/*.json irc/ # ---> Python diff --git a/LICENSE b/LICENSE index 547d63d..c33d4be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) . All rights reserved. +Copyright (c) 2018 Austin Ewens. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. diff --git a/actions/__init__.py b/actions/__init__.py index 3fed4d6..f8b1fd2 100644 --- a/actions/__init__.py +++ b/actions/__init__.py @@ -1,4 +1,8 @@ from actions.botlist import botlist +from actions.summon import summon +from actions.access import banish, pardon +from actions.control import puppet, nomad +from actions.stupid import hmm, hmmscore, hmmscoreboard actions = [ { @@ -10,5 +14,45 @@ actions = [ "type": "response", "pattern": "!rollcall", "callback": botlist + }, + { + "type": "response", + "pattern": ";;!summon \S+ .+", + "callback": summon + }, + { + "type": "response", + "pattern": ";;!banish \S+ .+", + "callback": banish + }, + { + "type": "response", + "pattern": ";;!pardon \S+", + "callback": pardon + }, + { + "type": "response", + "pattern": ";;!puppet [^|]+\|.+", + "callback": puppet + }, + { + "type": "response", + "pattern": ";;!nomad \S+ \S+", + "callback": nomad + }, + { + "type": "response", + "pattern": ";;hm+", + "callback": hmm + }, + { + "type": "response", + "pattern": ";;!hmmscore", + "callback": hmmscore + }, + { + "type": "response", + "pattern": "!hmmscoreboard", + "callback": hmmscoreboard } ] \ No newline at end of file diff --git a/actions/access.py b/actions/access.py new file mode 100644 index 0000000..e639df6 --- /dev/null +++ b/actions/access.py @@ -0,0 +1,37 @@ +from datetime import datetime + +def banish(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + user, reason = response.split("!banish ")[1].split(" ", 1) + + if name != author: + return + + if user not in self.bot.memories["users"]: + self.bot.memories["users"][user] = dict() + + self.bot.memories["users"][user]["blacklist"] = { + "reason": reason, + "when": datetime.now().timestamp() + } + + self.bot.save_memories() + + confirmation = "{} has been banished for reason: {}".format(user, reason) + self.bot.send_message(source, confirmation) + +def pardon(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + user = response.split("!pardon ")[1] + + if name != author: + return + + del self.bot.memories["users"][user]["blacklist"] + + self.bot.save_memories() + + confirmation = "{} has been pardoned".format(user) + self.bot.send_message(source, confirmation) \ No newline at end of file diff --git a/actions/botlist.py b/actions/botlist.py index 91ff332..1766a02 100644 --- a/actions/botlist.py +++ b/actions/botlist.py @@ -1,12 +1,15 @@ def botlist(self, name, source, response): - name = self.bot.botnick + botnick = self.bot.botnick author = self.bot.author email = self.bot.settings["email"] about = "the meta chat bot" commands = ", ".join([ "!botlist", - "!rollcall" + "!rollcall", + "!summon", + "!banish", + "!pardon" ]) - args = (name, author, email, about, commands) + args = (botnick, author, email, about, commands) message = "{} | {} <{}> | {} | {}".format(*args) self.bot.send_message(source, message) \ No newline at end of file diff --git a/actions/control.py b/actions/control.py new file mode 100644 index 0000000..dae0774 --- /dev/null +++ b/actions/control.py @@ -0,0 +1,27 @@ +def puppet(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + command = response.split("!puppet ")[1] + place, message = command.split("|", 1) + + if name != author: + return + + self.bot.send_message(place, message) + +def nomad(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + command = response.split("!nomad ")[1] + action, place = command.split(" ", 1) + + if name != author: + return + + actions = { + "join": self.bot.join, + "leave": self.bot.leave + } + default = lambda p: self.bot.send_message(source, "Invalid action!") + + actions.get(action, default)(place) \ No newline at end of file diff --git a/actions/nomad.py b/actions/nomad.py new file mode 100644 index 0000000..e69de29 diff --git a/actions/stupid.py b/actions/stupid.py new file mode 100644 index 0000000..5da6ca2 --- /dev/null +++ b/actions/stupid.py @@ -0,0 +1,58 @@ +import re +import operator + +def hmm(self, name, source, response): + botnick = self.bot.botnick + pattern = re.compile("hm+") + matches = re.findall(pattern, response) + score = len(matches) + + if name not in self.bot.memories["users"]: + self.bot.memories["users"][name] = dict() + + if "hmmscore" not in self.bot.memories["users"][name]: + self.bot.memories["users"][name]["hmmscore"] = 0 + + current_score = self.bot.memories["users"][name]["hmmscore"] + self.bot.memories["users"][name]["hmmscore"] = current_score + score + + self.bot.save_memories() + +def hmmscore(self, name, source, response): + botnick = self.bot.botnick + score = 0 + score_format = "Hmm score for '{}': {}" + + if " " in response: + name = response.split(" ", 1)[1] + + if name not in self.bot.memories["users"]: + self.bot.send_message(source, score_format.format(name, score)) + return + + if "hmmscore" in self.bot.memories["users"][name]: + score = self.bot.memories["users"][name]["hmmscore"] + self.bot.send_message(source, score_format.format(name, score)) + return + +def hmmscoreboard(self, name, source, response): + botnick = self.bot.botnick + hmmscores = list() + + for user, values in self.bot.memories["users"].items(): + hmmscores.append({ + "name": user, + "score": values.get("hmmscore", 0) + }) + + size = 3 + start = -size + + sort_scores = sorted(hmmscores, key=lambda k: k["score"]) + top_scores = sort_scores[start:][::-1] + + leaders = " | ".join([ + "{} {}".format(ts["name"], ts["score"]) for ts in top_scores + ]) + + self.bot.send_message(source, "Hmm Score Leaderboard: {}".format(leaders)) \ No newline at end of file diff --git a/actions/summon.py b/actions/summon.py new file mode 100644 index 0000000..647307e --- /dev/null +++ b/actions/summon.py @@ -0,0 +1,25 @@ +from subprocess import Popen, PIPE +from email.mime.text import MIMEText + +def summon(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + user, reason = response.split("!summon ")[1].split(" ", 1) + + email = "{}@tilde.team" + + message = MIMEText(" ".join([ + "My bot, {}, received a summoning request for you".format(botnick), + "from {} in channel {} for reason: {}".format(name, source, reason) + ])) + + message["From"] = email.format(botnick) + message["To"] = email.format(user) + message["Subject"] = "You have been summoned!" + + command = "/usr/sbin/sendmail -t -oi".split(" ") + p = Popen(command, stdin=PIPE, universal_newlines=True) + p.communicate(message.as_string()) + + confirmation = "{}: You have summoned {}".format(name, user) + self.bot.send_message(source, confirmation) \ No newline at end of file diff --git a/app.py b/app.py index 23355c7..27f4ffb 100755 --- a/app.py +++ b/app.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 +from os.path import dirname, realpath + from bot import Bot, Tasks, Responses from actions import actions -kickers = list() -inviters = dict() kingme = [ "#chaos" ] -bot = Bot("127.0.0.1", 6667, "BabiliBot|py", ["#bots"]) +bot = Bot("127.0.0.1", 6667, "BabiliBot|py", [ + "#bots", + "#insane" +]) responses = Responses(bot) for action in actions: @@ -20,7 +23,7 @@ for action in actions: action["callback"] ) -def try_to_king_me(bot, channel): +def try_to_king_me(channel): bot.send_message("ChanServ", "REGISTER {}", channel) bot.send_message("ChanServ", "SET Successor {} {}", channel, bot.botnick) bot.send_message("ChanServ", "SET Founder {} {}", channel, bot.author) @@ -30,26 +33,35 @@ def handle_pm(name, response): def handle_mode(channel, mode): if mode == "-r": - try_to_king_me(bot, channel) + try_to_king_me(channel) def handle_invite(channel, name): if channel in kingme: - try_to_king_me(bot, channel) + try_to_king_me(channel) - invites.append({ - "channel": channel, - "name": name - }) + users = bot.memories["users"] + if name not in users: + bot.memories["users"][name] = dict() -def handle_kick(kicker): - kickers.append(kicker) + if "invites" not in users[name]: + bot.memories["users"][name]["invites"] = list() + + bot.memories["users"][name]["invites"].append(channel) + +def handle_kick(name): + users = bot.memories["users"] + if name not in users: + bot.memories["users"][name] = dict() + + bot.memories["users"][name]["kicker"] = True def handle_message(name, source, response): - print("MSG: {} {} - {}".format(name, source, response)) responses.parse(name, source, response) + if response == "!debug": + print("::", bot.memories) if __name__ == "__main__": - bot.start("settings.json", { + bot.start(dirname(realpath(__file__)), { "pm": handle_pm, "mode": handle_mode, "invite": handle_invite, diff --git a/bot/core.py b/bot/core.py index 13141b6..838f17f 100644 --- a/bot/core.py +++ b/bot/core.py @@ -1,6 +1,7 @@ import re import json import socket +import os.path class Bot: def __init__(self, server, port, botnick, channels): @@ -12,7 +13,7 @@ class Bot: self.running = True self.settings = dict() - self.users = dict() + self.places = list() self.author = "" self.recv_size = 2048 @@ -29,8 +30,12 @@ class Bot: print("DEBUG: ", response) self.ircsock.send(response.encode()) + def send_action(self, target, message, *args): + self.send_message(target, "\001ACTION {}\001".format(message), *args) + def join(self, chan): self.send("JOIN {}", chan) + self.places.append(chan) message = "" magic_string = "End of /NAMES list." @@ -43,9 +48,18 @@ class Bot: raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ") prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u users = list(filter(prefix_filter, raw_users)) + remember = self.memories["users"] for user in users: - if user not in self.users: - self.users[user] = dict() + if user[0] == "~" or user[0] == "@": + user = user[1:] + + if user not in remember: + self.memories["users"][user] = dict() + + def leave(self, chan): + message = "PART {} :Bye-bye!" + self.send(message, chan) + self.places.remove(chan) def ping(self, message): response = message.split("PING :")[1] @@ -69,9 +83,9 @@ class Bot: def handle_rename(self, message): before, new_name = message.split("NICK ", 1) name = self.get_name(before) - user = self.users[name] - del self.users[name] - self.users[new_name] = user + user = self.memories["users"][name] + del self.memories["users"][name] + self.memories["users"][new_name] = user return user, new_name def handle_invite(self, message): @@ -86,12 +100,46 @@ class Bot: before, kicker = re.split(regex, message) return kicker + def handle_join(self, message): + before, after = message.split("JOIN ", 1) + user = self.get_name(before) + + if user not in self.memories["users"]: + self.memories["users"][user] = dict() + + return user + + def handle_part(self, message): + before, after = message.split("PART ", 1) + user = self.get_name(before) + return user + + def load_memories(self, location): + path = "{}/{}".format(self.location, location) + self.memories_path = path + + if not os.path.isfile(path): + self.memories = { + "users": dict() + } + else: + with open(path, "r") as f: + self.memories = json.loads(f.read()) + + def save_memories(self): + with open(self.memories_path, "w") as f: + try: + f.write(json.dumps(self.memories)) + except ValueError as e: + f.write("") + def load_settings(self, location): set_vars = [ "author" ] - with open(location, "r") as f: + path = "{}/{}".format(self.location, location) + with open(path, "r") as f: self.settings = json.loads(f.read()) for name, attr in self.settings.items(): @@ -102,7 +150,7 @@ class Bot: self.running = False self.send("QUIT") - def start(self, settings, callback): + def start(self, location, callback): message = "" registered = False confirmed = True @@ -111,7 +159,9 @@ class Bot: self.send("USER {0} {0} {0} {0}", self.botnick) self.send("NICK {0}", self.botnick) - self.load_settings(settings) + self.location = location + self.load_settings("settings.json") + self.load_memories("data/memories.json") password = self.settings["password"] or "" confirm = self.settings["confirm"] or "" @@ -169,6 +219,14 @@ class Bot: kicker = self.handle_kick(message) if "kick" in callback: callback["kick"](kicker) + elif "JOIN " in message: + user = self.handle_join(message) + if "join" in callback: + callback["join"](user) + elif "PART " in message: + user = self.handle_part(message) + if "part" in callback: + callback["part"](user) elif "INVITE " in message: channel, name = self.handle_invite(message) if "invite" in callback: diff --git a/bot/responses.py b/bot/responses.py index 39dbad6..dfacd2d 100644 --- a/bot/responses.py +++ b/bot/responses.py @@ -1,4 +1,5 @@ import re +from datetime import datetime class Responses: def __init__(self, bot): @@ -13,6 +14,32 @@ class Responses: if trigger_type in self.triggers: self.triggers[trigger_type][pattern] = callback + def allowed(self, name, source): + memories = self.bot.memories + users = memories["users"] + if name in users and "blacklist" in users[name]: + reason = users[name]["blacklist"]["reason"] + message = "is ignoring {} for reason '{}'".format(name, reason) + self.bot.send_action(source, message) + return False + + last_response = 0 + if "last_response" in self.bot.memories["users"][name]: + last_response = self.bot.memories["users"][name]["last_response"] + + now = datetime.now().timestamp() + author = self.bot.author + wait = 1 + + if name != author and last_response > 0 and now - last_response < wait: + self.bot.memories["users"][name]["blacklist"] = { + "reason": "Auto-banished", + "when": now + } + return False + + return True + def parse(self, name, source, response): check = { "name": name, @@ -20,11 +47,19 @@ class Responses: "response": response } + marker = ";;" + mlen = len(marker) + for trigger in list(self.triggers.keys()): for pattern, callback in self.triggers[trigger].items(): - if pattern[0] == "!" and pattern == response: - callback(self, name, source, response) + if pattern[:mlen] != marker: + if pattern == response and self.allowed(name, source): + callback(self, name, source, response) else: - regex = re.compile(pattern) + regex = re.compile(pattern[mlen:]) if regex.match(check[trigger]) is not None: - callback(self, name, source, response) \ No newline at end of file + if self.allowed(name, source): + callback(self, name, source, response) + + now = datetime.now().timestamp() + self.bot.memories["users"][name]["last_response"] = now \ No newline at end of file diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 From e7424f690b66dda9812ab6324373e1cf6c8878cc Mon Sep 17 00:00:00 2001 From: aewens Date: Thu, 13 Sep 2018 21:45:00 -0400 Subject: [PATCH 07/10] Added whois action --- actions/__init__.py | 6 ++++++ actions/nomad.py | 0 actions/web.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) delete mode 100644 actions/nomad.py create mode 100644 actions/web.py diff --git a/actions/__init__.py b/actions/__init__.py index f8b1fd2..d735748 100644 --- a/actions/__init__.py +++ b/actions/__init__.py @@ -3,6 +3,7 @@ from actions.summon import summon from actions.access import banish, pardon from actions.control import puppet, nomad from actions.stupid import hmm, hmmscore, hmmscoreboard +from actions.web import whois actions = [ { @@ -54,5 +55,10 @@ actions = [ "type": "response", "pattern": "!hmmscoreboard", "callback": hmmscoreboard + }, + { + "type": "response", + "pattern": ";;!whois \S+", + "callback": whois } ] \ No newline at end of file diff --git a/actions/nomad.py b/actions/nomad.py deleted file mode 100644 index e69de29..0000000 diff --git a/actions/web.py b/actions/web.py new file mode 100644 index 0000000..a07d690 --- /dev/null +++ b/actions/web.py @@ -0,0 +1,29 @@ +from urllib.request import Request, urlopen +from urllib.error import HTTPError +from json import loads + +def whois(self, name, source, response): + botnick = self.bot.botnick + domain = response.split("!whois ")[1] + + api_url = "https://api.jsonwhoisapi.com/v1/whois" + api_key = self.bot.settings.get("extras", dict()).get("jsonwhoisapi", "") + + req = Request("{}?identifier={}".format(api_url, domain)) + req.add_header("Authorization", api_key) + + try: + data = loads(urlopen(req).read()) + except HTTPError: + self.bot.send_message(source, "{} cannot exist".format(domain)) + return + + registered = data.get("registered", None) + if registered is not None: + nameservers = len(data.get("nameservers", "")) + registrar = data.get("registrar", dict()) + is_registered = "id" in registrar or nameservers > 0 + status = "registered" if is_registered else "available" + self.bot.send_message(source, "{} is '{}'".format(domain, status)) + else: + self.bot.send_message(source, "{} might be available".format(domain)) \ No newline at end of file From 746ed85333a9ec0c31ad9f47d7db8f9dc51bdb46 Mon Sep 17 00:00:00 2001 From: aewens Date: Sat, 15 Sep 2018 20:11:02 -0400 Subject: [PATCH 08/10] Added pushbullet to summon, added in tasks/coroutines system, added in bbj integration --- actions/__init__.py | 3 +- actions/summon.py | 25 ------------ actions/web.py | 72 ++++++++++++++++++++++++++++++++++ app.py | 24 ++++++++---- bot/core.py | 5 +++ bot/tasks.py | 38 +++++++++++++++++- coroutines/__init__.py | 19 +++++++++ coroutines/bbj.py | 88 ++++++++++++++++++++++++++++++++++++++++++ coroutines/rss.py | 0 9 files changed, 238 insertions(+), 36 deletions(-) delete mode 100644 actions/summon.py create mode 100644 coroutines/__init__.py create mode 100644 coroutines/bbj.py create mode 100644 coroutines/rss.py diff --git a/actions/__init__.py b/actions/__init__.py index d735748..f3fa1a9 100644 --- a/actions/__init__.py +++ b/actions/__init__.py @@ -1,9 +1,8 @@ from actions.botlist import botlist -from actions.summon import summon +from actions.web import summon, whois from actions.access import banish, pardon from actions.control import puppet, nomad from actions.stupid import hmm, hmmscore, hmmscoreboard -from actions.web import whois actions = [ { diff --git a/actions/summon.py b/actions/summon.py deleted file mode 100644 index 647307e..0000000 --- a/actions/summon.py +++ /dev/null @@ -1,25 +0,0 @@ -from subprocess import Popen, PIPE -from email.mime.text import MIMEText - -def summon(self, name, source, response): - botnick = self.bot.botnick - author = self.bot.author - user, reason = response.split("!summon ")[1].split(" ", 1) - - email = "{}@tilde.team" - - message = MIMEText(" ".join([ - "My bot, {}, received a summoning request for you".format(botnick), - "from {} in channel {} for reason: {}".format(name, source, reason) - ])) - - message["From"] = email.format(botnick) - message["To"] = email.format(user) - message["Subject"] = "You have been summoned!" - - command = "/usr/sbin/sendmail -t -oi".split(" ") - p = Popen(command, stdin=PIPE, universal_newlines=True) - p.communicate(message.as_string()) - - confirmation = "{}: You have summoned {}".format(name, user) - self.bot.send_message(source, confirmation) \ No newline at end of file diff --git a/actions/web.py b/actions/web.py index a07d690..ddc2895 100644 --- a/actions/web.py +++ b/actions/web.py @@ -1,7 +1,79 @@ +from subprocess import Popen, PIPE +from email.mime.text import MIMEText + from urllib.request import Request, urlopen +from urllib.parse import urlencode from urllib.error import HTTPError from json import loads +def get_iden(devices, device_name): + for device in devices: + if device.get("nickname", "") == device_name: + return device.get("iden", "") + +def push_note(bot, title, body): + api_url = "https://api.pushbullet.com/v2" + extra_settings = bot.settings.get("extras", dict()) + pb_settings = extra_settings.get("pushbullet", dict()) + api_key = pb_settings.get("api", "") + device_name = pb_settings.get("device", "") + + list_devices = Request("{}/devices".format(api_url)) + list_devices.add_header("Access-Token", api_key) + + try: + data = loads(urlopen(list_devices).read()) + except HTTPError: + return + + devices = data.get("devices", list()) + iden = get_iden(devices, device_name) + + params = { + "device_iden": iden, + "type": "note", + "title": title, + "body": body + } + + post_params = urlencode(params).encode() + + pushes = Request("{}/pushes".format(api_url), post_params) + pushes.add_header("Access-Token", api_key) + + try: + response = loads(urlopen(pushes).read()) + except HTTPError as e: + return + +def summon(self, name, source, response): + botnick = self.bot.botnick + author = self.bot.author + user, reason = response.split("!summon ")[1].split(" ", 1) + + email = "{}@tilde.team" + subject = "You have been summoned!" + + text = " ".join([ + "My bot, {}, received a summoning request for you".format(botnick), + "from {} in channel {} for reason: {}".format(name, source, reason) + ]) + message = MIMEText(text) + + message["From"] = email.format(botnick) + message["To"] = email.format(user) + message["Subject"] = subject + + command = "/usr/sbin/sendmail -t -oi".split(" ") + p = Popen(command, stdin=PIPE, universal_newlines=True) + p.communicate(message.as_string()) + + if user == author: + push_note(self.bot, subject, text) + + confirmation = "{}: You have summoned {}".format(name, user) + self.bot.send_message(source, confirmation) + def whois(self, name, source, response): botnick = self.bot.botnick domain = response.split("!whois ")[1] diff --git a/app.py b/app.py index 27f4ffb..d0f6d54 100755 --- a/app.py +++ b/app.py @@ -4,16 +4,17 @@ from os.path import dirname, realpath from bot import Bot, Tasks, Responses from actions import actions +from coroutines import coroutines -kingme = [ - "#chaos" -] +debug = False +kingme = [] if debug else ["#chaos"] +channels = ["#bots", "#insane"] +# if not debug: +# channels.extend([]) -bot = Bot("127.0.0.1", 6667, "BabiliBot|py", [ - "#bots", - "#insane" -]) +bot = Bot("127.0.0.1", 6667, "BabiliBot|py", channels) responses = Responses(bot) +tasks = Tasks(bot) for action in actions: if "type" in action and "pattern" in action and "callback" in action: @@ -23,6 +24,14 @@ for action in actions: action["callback"] ) +# for coro in coroutines: +# worker = coro["worker"] +# interval = coro["interval"] +# state = coro.get("state", None) +# coro_state = state if state is not None else (bot,) +# tasks.add_coroutine(worker, interval, coro_state) +tasks.coroutines = coroutines + def try_to_king_me(channel): bot.send_message("ChanServ", "REGISTER {}", channel) bot.send_message("ChanServ", "SET Successor {} {}", channel, bot.botnick) @@ -61,6 +70,7 @@ def handle_message(name, source, response): print("::", bot.memories) if __name__ == "__main__": + bot.tasks = tasks bot.start(dirname(realpath(__file__)), { "pm": handle_pm, "mode": handle_mode, diff --git a/bot/core.py b/bot/core.py index 838f17f..f1511b0 100644 --- a/bot/core.py +++ b/bot/core.py @@ -14,6 +14,7 @@ class Bot: self.settings = dict() self.places = list() + self.tasks = None self.author = "" self.recv_size = 2048 @@ -195,6 +196,10 @@ class Bot: print("DEBUG: Joined") + if self.tasks is not None: + if getattr(self.tasks, "run", None) is not None: + self.tasks.run() + while self.running: message = self.ircsock.recv(self.recv_size).decode() message = message.strip(self.splitter) diff --git a/bot/tasks.py b/bot/tasks.py index 71c56e8..4718c7d 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -1,3 +1,37 @@ +import time +import sched +from threading import Thread + class Tasks: - def __init__(self): - pass \ No newline at end of file + def __init__(self, bot): + self.bot = bot + self.scheduler = sched.scheduler(time.time, time.sleep) + self.thread = Thread(target=self.worker, args=(self,)) + self.coroutines = list() + self.states = dict() + + def periodic(self, scheduler, interval, action, index, state=dict()): + self.states[index] = action(state) + scheduler.enter(interval, 1, self.periodic, ( + scheduler, interval, action, index, self.states[index] + )) + + def worker(self, tasks): + for c, coro in enumerate(tasks.coroutines): + interval = coro["interval"] + worker = coro["worker"] + state = coro.get("state", dict()) + state["bot"] = tasks.bot + tasks.periodic(tasks.scheduler, interval, worker, c, state) + tasks.scheduler.run() + + def add_coroutine(self, worker, interval, state=dict()): + self.coroutines.append({ + "worker": worker, + "interval": interval, + "state": state + }) + + def run(self): + self.thread.daemon = True + self.thread.start() \ No newline at end of file diff --git a/coroutines/__init__.py b/coroutines/__init__.py new file mode 100644 index 0000000..07549b1 --- /dev/null +++ b/coroutines/__init__.py @@ -0,0 +1,19 @@ +from coroutines.bbj import BBJ + +# { +# "worker": test, +# "interval": 3 +# } +# def test(bot): +# print("Testing {}".format(bot.botnick)) + +coroutines = [ + { + "worker": lambda state: BBJ(state).start(), + "interval": 5, + "state": { + "source": "http://localhost:7099/api", + "channels": ["#insane"], #team + } + } +] \ No newline at end of file diff --git a/coroutines/bbj.py b/coroutines/bbj.py new file mode 100644 index 0000000..159a283 --- /dev/null +++ b/coroutines/bbj.py @@ -0,0 +1,88 @@ +from urllib.request import Request, urlopen +from urllib.parse import urlencode +from urllib.error import HTTPError +from datetime import datetime +from json import loads, dumps +from re import sub + +class BBJ: + def __init__(self, state): + self.name = "bbj" + self.bot = state["bot"] + self.source = state["source"] + self.channels = state["channels"] + self.memory = state.get("memory", { + "initialized": False, + # "timestamp": datetime.now().timestamp(), + "known": dict() + }) + + def start(self): + if not self.memory["initialized"]: + self.memory["initialized"] = True + self.fetch(self.cache) + return self.run() + + def run(self): + self.fetch(self.mirror) + return { + "bot": self.bot, + "source": self.source, + "channels": self.channels, + "memory": self.memory + } + + def cache(self, item): + self.memory["known"][item["thread_id"]] = item["last_mod"] + + def process_thread(self, thread_id, thread): + data = thread.get("data", dict()) + title = data.get("title", "") + replies = data.get("reply_count", "") + messages = data.get("messages", "") + usermap = thread.get("usermap", dict()) + reply = messages[replies] + author = reply.get("author", "") + username = usermap[author].get("user_name", "") + body = reply.get("body", "") + body = sub(r">>\d\n\n", r"", body) + body = sub(r"\n", r" ", body) + php = "https://bbj.tilde.team/index.php" + link = "{}?thread_id={}".format(php, thread_id) + for channel in self.channels: + response = "'{}' ({}) : {} <{}>".format(title, username, body, link) + message = "[{}] {}".format(self.name, response) + self.bot.send_message(channel, message) + + def get_thread(self, thread_id, callback): + params = { + "thread_id": thread_id + } + post_params = str(dumps(params)).encode() + thread_load = Request("{}/thread_load".format(self.source), post_params) + thread_load.add_header("Content-Type", "application/json") + + try: + response = callback(thread_id, loads(urlopen(thread_load).read())) + except HTTPError: + return + + def mirror(self, item): + thread_id = item["thread_id"] + last_mod = self.memory["known"][thread_id] + if last_mod == item["last_mod"]: + return + + self.memory["known"][thread_id] = item["last_mod"] + self.get_thread(thread_id, self.process_thread) + + def fetch(self, callback): + thread_index = Request("{}/thread_index".format(self.source)) + + try: + response = loads(urlopen(thread_index).read()) + threads = response.get("data", dict()) + for thread in threads: + callback(thread) + except HTTPError: + return \ No newline at end of file diff --git a/coroutines/rss.py b/coroutines/rss.py new file mode 100644 index 0000000..e69de29 From 97cdb98542e6e219a95e1b1e6a18e59d7ad0e8ff Mon Sep 17 00:00:00 2001 From: aewens Date: Sat, 15 Sep 2018 21:27:56 -0400 Subject: [PATCH 09/10] Master release candidate 1, added alias to bbj, added RSS support for tilde.news links/comments --- coroutines/__init__.py | 24 ++++++++++++++- coroutines/bbj.py | 7 +++-- coroutines/rss.py | 69 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/coroutines/__init__.py b/coroutines/__init__.py index 07549b1..fc344f0 100644 --- a/coroutines/__init__.py +++ b/coroutines/__init__.py @@ -1,4 +1,5 @@ from coroutines.bbj import BBJ +from coroutines.rss import RSS # { # "worker": test, @@ -12,8 +13,29 @@ coroutines = [ "worker": lambda state: BBJ(state).start(), "interval": 5, "state": { + "alias": "bbj", "source": "http://localhost:7099/api", - "channels": ["#insane"], #team + "channels": ["#bots"] #team + } + }, + { + "worker": lambda state: RSS(state).start(), + "interval": 6, + "state": { + "alias": "title", + "source": "https://tilde.news/newest.rss", + "use": "title", + "channels": ["#bots"] # "#meta", "#tildeverse" + } + }, + { + "worker": lambda state: RSS(state).start(), + "interval": 8, + "state": { + "alias": "links-comments", + "source": "https://tilde.news/comments.rss", + "use": "summary", + "channels": ["#bots"] #tildeverse } } ] \ No newline at end of file diff --git a/coroutines/bbj.py b/coroutines/bbj.py index 159a283..4880019 100644 --- a/coroutines/bbj.py +++ b/coroutines/bbj.py @@ -7,13 +7,13 @@ from re import sub class BBJ: def __init__(self, state): - self.name = "bbj" + self.name = "BBJ" self.bot = state["bot"] + self.alias = state["alias"] self.source = state["source"] self.channels = state["channels"] self.memory = state.get("memory", { "initialized": False, - # "timestamp": datetime.now().timestamp(), "known": dict() }) @@ -27,6 +27,7 @@ class BBJ: self.fetch(self.mirror) return { "bot": self.bot, + "alias": self.alias, "source": self.source, "channels": self.channels, "memory": self.memory @@ -51,7 +52,7 @@ class BBJ: link = "{}?thread_id={}".format(php, thread_id) for channel in self.channels: response = "'{}' ({}) : {} <{}>".format(title, username, body, link) - message = "[{}] {}".format(self.name, response) + message = "[{}] {}".format(self.alias, response) self.bot.send_message(channel, message) def get_thread(self, thread_id, callback): diff --git a/coroutines/rss.py b/coroutines/rss.py index e69de29..90dcd44 100644 --- a/coroutines/rss.py +++ b/coroutines/rss.py @@ -0,0 +1,69 @@ +from xml.etree import ElementTree as etree +from urllib.request import Request, urlopen +from urllib.error import HTTPError +from json import loads, dumps +from re import sub + +class RSS: + def __init__(self, state): + self.name = "RSS" + self.bot = state["bot"] + self.alias = state["alias"] + self.use = state["use"] + self.source = state["source"] + self.channels = state["channels"] + self.memory = state.get("memory", { + "initialized": False, + "known": list() + }) + + def start(self): + if not self.memory["initialized"]: + self.memory["initialized"] = True + self.fetch(self.cache) + return self.run() + + def run(self): + self.fetch(self.mirror) + return { + "bot": self.bot, + "alias": self.alias, + "use": self.use, + "source": self.source, + "channels": self.channels, + "memory": self.memory + } + + def cache(self, item): + guid = item.findtext("guid", None) + if guid is not None: + self.memory["known"].append(guid) + + def mirror(self, item): + guid = item.findtext("guid", None) + if guid is None: + return + + if guid in self.memory["known"]: + return + + self.memory["known"].append(guid) + + use = sub(r"(<\/?[^>]+>)|\\n", "", item.findtext([self.use], "")) + user = item.findtext("author", "").split("@")[0] + post = "{} (posted by {}) <{}>".format(use, user, guid) + response = "[{}] {}".format(self.alias, post) + for channel in self.channels: + self.bot.send_message(channel, response) + + def fetch(self, callback): + req = Request(self.source) + try: + response = urlopen(req).read() + except HTTPError: + return + + feed = etree.fromstring(response) + items = feed.findall("channel/item") + for item in items: + callback(item) \ No newline at end of file From d90d1a9567537a03870a3745ccbce012d78ee8a8 Mon Sep 17 00:00:00 2001 From: aewens Date: Sun, 16 Sep 2018 00:27:38 -0400 Subject: [PATCH 10/10] Bug fixes to handle spam / capital letters --- .gitignore | 1 + actions/botlist.py | 5 ++++- app.py | 11 ++++++++--- bot/responses.py | 8 ++++++++ coroutines/__init__.py | 6 +++--- coroutines/rss.py | 2 +- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index d8b6c30..f1d5ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # App-specific settings.json +settings.demo.json data/*.json irc/ diff --git a/actions/botlist.py b/actions/botlist.py index 1766a02..54ce622 100644 --- a/actions/botlist.py +++ b/actions/botlist.py @@ -8,7 +8,10 @@ def botlist(self, name, source, response): "!rollcall", "!summon", "!banish", - "!pardon" + "!pardon", + "!hmmscore", + "!hmmscoreboard", + "!whois" ]) args = (botnick, author, email, about, commands) message = "{} | {} <{}> | {} | {}".format(*args) diff --git a/app.py b/app.py index d0f6d54..4e170e3 100755 --- a/app.py +++ b/app.py @@ -9,10 +9,15 @@ from coroutines import coroutines debug = False kingme = [] if debug else ["#chaos"] channels = ["#bots", "#insane"] -# if not debug: -# channels.extend([]) +if not debug: + channels.extend([ + "#meta", + "#team", + "#chaos", + "#tildeverse" + ]) -bot = Bot("127.0.0.1", 6667, "BabiliBot|py", channels) +bot = Bot("127.0.0.1", 6667, "BabiliBot", channels) responses = Responses(bot) tasks = Tasks(bot) diff --git a/bot/responses.py b/bot/responses.py index dfacd2d..ae50f6e 100644 --- a/bot/responses.py +++ b/bot/responses.py @@ -17,6 +17,9 @@ class Responses: def allowed(self, name, source): memories = self.bot.memories users = memories["users"] + if name not in users: + return False + if name in users and "blacklist" in users[name]: reason = users[name]["blacklist"]["reason"] message = "is ignoring {} for reason '{}'".format(name, reason) @@ -41,6 +44,11 @@ class Responses: return True def parse(self, name, source, response): + users = self.bot.memories["users"] + if name not in users: + return False + + response = response.lower() check = { "name": name, "source": source, diff --git a/coroutines/__init__.py b/coroutines/__init__.py index fc344f0..bd7b4da 100644 --- a/coroutines/__init__.py +++ b/coroutines/__init__.py @@ -15,7 +15,7 @@ coroutines = [ "state": { "alias": "bbj", "source": "http://localhost:7099/api", - "channels": ["#bots"] #team + "channels": ["#team"] } }, { @@ -25,7 +25,7 @@ coroutines = [ "alias": "title", "source": "https://tilde.news/newest.rss", "use": "title", - "channels": ["#bots"] # "#meta", "#tildeverse" + "channels": ["#meta", "#tildeverse"] } }, { @@ -35,7 +35,7 @@ coroutines = [ "alias": "links-comments", "source": "https://tilde.news/comments.rss", "use": "summary", - "channels": ["#bots"] #tildeverse + "channels": ["#tildeverse"] } } ] \ No newline at end of file diff --git a/coroutines/rss.py b/coroutines/rss.py index 90dcd44..b24d3c1 100644 --- a/coroutines/rss.py +++ b/coroutines/rss.py @@ -49,7 +49,7 @@ class RSS: self.memory["known"].append(guid) - use = sub(r"(<\/?[^>]+>)|\\n", "", item.findtext([self.use], "")) + use = sub(r"(<\/?[^>]+>)|\\n", "", item.findtext(self.use, "")) user = item.findtext("author", "").split("@")[0] post = "{} (posted by {}) <{}>".format(use, user, guid) response = "[{}] {}".format(self.alias, post)