#!/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. """ if not bot.memory.get("watcher"): bot.memory["watcher"] = {} con = bot.db.connect() cur = con.cursor() try: watching = cur.execute("SELECT * FROM watcher").fetchall() except: cur.execute("CREATE TABLE watcher(" "api_url TEXT PRIMARY KEY," "name TEXT DEFAULT 'Anonymous'," "last_post INTEGER," "time_since TEXT," "channel TEXT)") con.commit() else: for thread in watching: if get_thread_url(thread[0]) in bot.memory["watcher"].keys(): continue t = WatcherThread(bot, *thread) t.start() bot.memory["watcher"][get_thread_url(thread[0])] = t con.close() 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 OPName") def watch(bot, trigger): """ A thread watcher for 4chan. """ if len(trigger.args) < 2: boy.reply("What thread?") url = trigger.args[1] name = trigger.args[2] if len(trigger.args) >= 3 else "Anonymous" if url in bot.memory["watcher"].keys(): return bot.msg("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.msg("404: thread not found") thread = res.json() last_post = get_last_post(thread, name) time_since = get_time() t = WatcherThread(bot, api_url, name,last_post, time_since,trigger.channel) t.start() bot.memory["watcher"][url] = t bot.db.execute( "INSERT INTO watcher(api_url, name, last_post, time_since, channel) " "VALUES(?,?,?,?,?)", (api_url, name, last_post, time_since, trigger.channel)) bot.msg("[\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. """ if len(trigger.args) < 2: boy.reply("What thread?") url = trigger.args[1] try: bot.memory["watcher"][url].stop.set() bot.memory["watcher"].pop(url) except KeyError: return bot.msg("Error: I'm not watching that thread.") removeThread(bot, get_api_url(url),) bot.msg("[\x0304Watcher\x03] No longer watching: \x0307" + url) def removeThread(bot, url): """ Removes the provided thread from the database. This should be the API url. """ bot.db.execute("DELETE FROM watcher WHERE api_url = ?", (url,)) class WatcherThread(threading.Thread): def __init__(self, bot, api_url, name, last_post, time_since, channel): threading.Thread.__init__(self) self.stop = threading.Event() self.period = 20 self.bot = bot self.channel = channel self.api_url = api_url self.name = name self.last_post = last_post self.time_since = time_since def run(self): while not self.stop.is_set(): self.stop.wait(self.period) headers = {"If-Modified-Since": self.time_since} try: res = requests.get(self.api_url, headers=headers, verify=True) self.time_since = get_time() except urllib3.exceptions.NewConnectionError: print(f"Watcher: Thread {self.api_url}: Connection error") continue if res.status_code == 404: msg = "[\x0304Watcher\x03] Thread deleted: " msg += f"\x0307{get_thread_url(self.api_url)}" self.bot.msg(self.channel, msg) removeThread(self.bot, self.api_url) self.stop.set() continue if res.status_code == 304: continue thread = res.json() if thread["posts"][0].get("closed"): msg = "[\x0304Watcher\x03] Thread closed: " msg += f"\x0307{get_thread_url(self.api_url)}" self.bot.msg(self.channel, msg) removeThread(self.bot, self.api_url) self.stop.set() continue new_last_post = get_last_post(thread, self.name) if new_last_post > self.last_post: self.last_post = new_last_post print(self.last_post) msg = "[\x0304Watcher\x03] New post from \x0308" msg += f"{self.name}\x03: \x0307{get_thread_url(self.api_url)}" msg += f"#{self.last_post}" self.bot.msg(self.channel, msg)