diff --git a/modules/watcher.py b/modules/watcher.py new file mode 100755 index 0000000..7b54a26 --- /dev/null +++ b/modules/watcher.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +""" +A thread watcher module for 4chan. +""" +import time +import threading + +import requests + +from module import commands, example + +def setup(bot): + """ + Establishes the bot's dictionary of watched threads. + """ + bot.memory["watcher"] = {} + + +def get_time(): + """ + Returns the current time formatted in If-Modified-Since notation. + """ + return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + + +def get_num_posts(thread, name): + """ + Gets the number of OP posts from the thread JSON. + """ + num = 0 + for post in thread["posts"]: + if post.get("name") == name: + num += 1 + return num + + +def get_last_post(thread, name): + """ + Gets the last post made by name. + """ + for post in reversed(thread["posts"]): + if post.get("name") == name: + return post.get("no") + + +def get_api_url(url): + """ + Returns the API url for the provided thread url. + """ + return url.replace("boards.4chan", "a.4cdn") + ".json" + + +def get_thread_url(api_url): + """ + Returns the normal thread url for the provided API url. + """ + return api_url.replace("a.4cdn", "boards.4chan").replace(".json", "") + + +@commands("watch") +@example(".watch https://boards.4chan.org/qst/thread/2") +def watch(bot, trigger): + """ + A thread watcher for 4chan. + """ + url = trigger.group(3) + op_name = trigger.group(4) + + if url in bot.memory["watcher"].keys(): + return bot.say("Error: I'm already watching that thread.") + + api_url = get_api_url(url) + res = requests.get(api_url, verify=True) + if res.status_code == 404: + return bot.say("404: thread not found") + + thread = res.json() + last_post = get_last_post(thread, op_name) + time_since = get_time() + + t = WatcherThread(bot, api_url, op_name, last_post, time_since) + t.start() + bot.memory["watcher"][url] = t + bot.say("[\x0304Watcher\x03] Watching thread: \x0307" + url) + + +@commands("unwatch") +@example(".unwatch https://boards.4chan.org/qst/thread/2") +def unwatch(bot, trigger): + """ + Stops the thread watcher thread for that thread. + """ + url = trigger.group(2) + try: + bot.memory["watcher"][url].stop.set() + bot.memory["watcher"].pop(url) + except KeyError: + return bot.say("Error: I'm not watching that thread.") + return bot.say("[\x0304Watcher\x03] No longer watching: \x0307" + url) + + +class WatcherThread(threading.Thread): + def __init__(self, bot, api_url, op_name, last_post, time_since): + threading.Thread.__init__(self) + self.stop = threading.Event() + self.period = 20 + + self.bot = bot + self.api_url = api_url + self.op_name = op_name + self.last_post = last_post + self.time_since = time_since + + + def run(self): + while not self.stop.is_set(): + headers = {"If-Modified-Since": self.time_since} + res = requests.get(self.api_url, headers=headers, verify=True) + self.time_since = get_time() + + if res.status_code == 404: + msg = "[\x0304Watcher\x03] Thread deleted: " \ + + f"\x0307{get_thread_url(self.api_url)}" + self.bot.say(msg) + self.stop.set() + continue + + if res.status_code == 304: + self.stop.wait(self.period) + continue + + thread = res.json() + if thread["posts"][0].get("closed"): + msg = "[\x0304Watcher\x03] Thread closed: " \ + + f"\x0307{get_thread_url(self.api_url)}" + self.bot.say(msg) + self.stop.set() + continue + + new_last_post = get_last_post(thread, self.op_name) + if new_last_post > self.last_post: + self.last_post = new_last_post + msg = "[\x0304Watcher\x03] New post from \x0308" \ + + f"{self.op_name}\x03 in \x0307{get_thread_url(self.api_url)}" + self.bot.say(msg) + self.stop.wait(self.period)