195 lines
4.6 KiB
Python
Executable File
195 lines
4.6 KiB
Python
Executable File
#!/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 STRING PRIMARY KEY,"
|
|
"name STRING DEFAULT 'Anonymous',"
|
|
"last_post INT,"
|
|
"time_since STRING,"
|
|
"channel STRING)")
|
|
cur.commit()
|
|
con.close()
|
|
else:
|
|
for thread in watching:
|
|
if get_thread_url(thread[0]) in bot.memory["watcher"].keys():
|
|
continue
|
|
t = WatcherThread(bot, thread[0], thread[1], thread[2], thread[3],
|
|
thread[4])
|
|
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")
|
|
def watch(bot, trigger):
|
|
"""
|
|
A thread watcher for 4chan.
|
|
"""
|
|
url = trigger.group(3)
|
|
name = trigger.group(4)
|
|
if not name:
|
|
name = "Anonymous"
|
|
|
|
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, name)
|
|
time_since = get_time()
|
|
|
|
t = WatcherThread(bot, api_url, name, last_post, time_since, trigger.sender)
|
|
t.start()
|
|
bot.memory["watcher"][url] = t
|
|
|
|
con = bot.db.connect()
|
|
cur = con.cursor()
|
|
cur.execute("INSERT INTO watcher(api_url, name, last_post, time_since, channel)"
|
|
" VALUES(?,?,?,?,?)", (api_url, name, last_post, time_since,
|
|
trigger.sender))
|
|
con.commit()
|
|
con.close()
|
|
|
|
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.")
|
|
|
|
con = bot.db.connect()
|
|
cur = con.cursor()
|
|
cur.execute("DELETE FROM watcher WHERE api_url = ?", (get_api_url(url),))
|
|
con.commit()
|
|
con.close()
|
|
|
|
bot.say("[\x0304Watcher\x03] No longer watching: \x0307" + 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.api_url = api_url
|
|
self.name = name
|
|
self.last_post = last_post
|
|
self.time_since = time_since
|
|
self.channel = channel
|
|
|
|
|
|
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: " \
|
|
+ f"\x0307{get_thread_url(self.api_url)}"
|
|
self.bot.say(msg, self.channel)
|
|
self.stop.set()
|
|
continue
|
|
|
|
if res.status_code == 304:
|
|
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.channel)
|
|
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
|
|
msg = "[\x0304Watcher\x03] New post from \x0308" \
|
|
+ f"{self.name}\x03 in \x0307{get_thread_url(self.api_url)}"
|
|
self.bot.say(msg, self.channel)
|
|
|