diff options
Diffstat (limited to 'cgi/post.py')
-rw-r--r-- | cgi/post.py | 1260 |
1 files changed, 1260 insertions, 0 deletions
diff --git a/cgi/post.py b/cgi/post.py new file mode 100644 index 0000000..f0ee814 --- /dev/null +++ b/cgi/post.py @@ -0,0 +1,1260 @@ +# coding=utf-8 +import math +import os +import shutil +import time +import threading +import Queue +import _mysql +import formatting + +from database import * +from template import * +from settings import Settings +from framework import * + +class Post(object): + def __init__(self, boardid=0): + self.post = { + "boardid": boardid, + "parentid": 0, + "name": "", + "tripcode": "", + "email": "", + "subject": "", + "message": "", + "password": "", + "file": "", + "file_hex": "", + "file_size": 0, + "thumb": "", + "image_width": 0, + "image_height": 0, + "thumb_width": 0, + "thumb_height": 0, + "ip": "", + "timestamp_formatted": "", + "timestamp": 0, + "bumped": 0, + "locked": 0, + } + + def __getitem__(self, key): + return self.post[key] + + def __setitem__(self, key, value): + self.post[key] = value + + def __iter__(self): + return self.post + + def insert(self): + logTime("Insertando Post") + post_values = [_mysql.escape_string(str(value)) for key, value in self.post.iteritems()] + + return InsertDb("INSERT INTO `posts` (`%s`) VALUES ('%s')" % ( + "`, `".join(self.post.keys()), + "', '".join(post_values) + )) + +class RegenerateThread(threading.Thread): + def __init__(self, threadid, request_queue): + threading.Thread.__init__(self, name="RegenerateThread-%d" % (threadid,)) + self.request_queue = request_queue + self.board = Settings._.BOARD + + def run(self): + Settings._.BOARD = self.board + while 1: + action = self.request_queue.get() + if action is None: + break + if action == "front": + regenerateFrontPages() + else: + regenerateThreadPage(action) + +def threadNumReplies(post): + """ + Get how many replies a thread has + """ + board = Settings._.BOARD + + num = FetchOne("SELECT COUNT(1) FROM `posts` WHERE `parentid` = '%s' AND `boardid` = '%s'" % (post, board['id']), 0) + return int(num[0])+1 + +def get_parent_post(post_id, board_id): + post = FetchOne("SELECT `id`, `email`, `message`, `locked`, `subject`, `timestamp`, `bumped`, `last`, `length` FROM `posts` WHERE `id` = %s AND `parentid` = 0 AND `IS_DELETED` = 0 AND `boardid` = %s LIMIT 1" % (post_id, board_id)) + if post: + return post + else: + raise UserError, _("The ID of the parent post is invalid.") + +def getThread(postid=0, mobile=False, timestamp=0): + board = Settings._.BOARD + total_bytes = 0 + + database_lock.acquire() + try: + if timestamp: + cond = "`timestamp` = %s" % str(timestamp) + else: + cond = "`id` = %s" % str(postid) + + op_post = FetchOne("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE %s AND `boardid` = %s AND parentid = 0 LIMIT 1" % (cond, board["id"])) + if op_post: + op_post['num'] = 1 + if mobile: + op_post['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", op_post['timestamp_formatted']) + thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0} + #thread = {"id": op_post["id"], "posts": [op_post], "omitted": 0, "omitted_img": 0} + total_bytes += len(op_post["message"])+80 + + replies = FetchAll("SELECT IS_DELETED, email, file, file_size, id, image_height, image_width, ip, message, name, subject, thumb, thumb_height, thumb_width, timestamp_formatted, tripcode, parentid, locked, expires, expires_alert, expires_formatted, timestamp FROM `posts` WHERE `parentid` = %s AND `boardid` = %s ORDER BY `id` ASC" % (op_post["id"], board["id"])) + thread["length"] = 1 + if replies: + for reply in replies: + thread["length"] += 1 + reply['num'] = thread["length"] + if mobile: + reply['message'] = formatting.fixMobileLinks(reply['message']) + reply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", reply['timestamp_formatted']) + thread["posts"].append(reply) + total_bytes += len(reply["message"])+57 + + # An imageboard needs subject + if board["board_type"] in ['1', '5']: + thread["timestamp"] = op_post["timestamp"] + thread["subject"] = op_post["subject"] + thread["message"] = op_post["message"] + thread["locked"] = op_post["locked"] + thread["size"] = "%d KB" % int(total_bytes / 1000) + + #threads = [thread] + else: + return None + finally: + database_lock.release() + + return thread + +def getID(threadid, postnum): + board = Settings._.BOARD + + database_lock.acquire() + try: + posts = FetchAll("SELECT id FROM `posts` WHERE `parentid`=%s AND `boardid`=%s ORDER BY `id` ASC" % (thread["id"], board["id"])) + if posts: + post = posts[int(postnum)-1] + postid = post["id"] + else: + return None + finally: + database_lock.release() + + return postid + +def shortenMsg(message, elid='0', elboard='0'): + """ + Intenta acortar el mensaje si es necesario + Algoritmo traducido desde KusabaX + """ + board = Settings._.BOARD + + limit = 100 * int(board['numline']) + + message_exploded = message.split('<br />') + if len(message) > limit or len(message_exploded) > int(board['numline']): + message_shortened = '' + for i in range(int(board['numline'])): + if i >= len(message_exploded): + break + + message_shortened += message_exploded[i] + '<br />' + + #try: + message_shortened = message_shortened.decode('utf-8', 'replace') + #except: + + if len(message_shortened) > limit: + message_shortened = message_shortened[:limit] + + message_shortened = formatting.close_html(message_shortened) + + return True, message_shortened + else: + return False, message + +def threadUpdated(postid): + """ + Shortcut to update front pages and thread page by passing a thread ID. Uses + the simple threading module to do both regenerateFrontPages() and + regenerateThreadPage() asynchronously + """ + # Use queues only if multithreading is enabled + if Settings.USE_MULTITHREADING: + request_queue = Queue.Queue() + threads = [RegenerateThread(i, request_queue) for i in range(2)] + for t in threads: + t.start() + + request_queue.put("front") + request_queue.put(postid) + + for i in range(2): + request_queue.put(None) + + for t in threads: + t.join + else: + regenerateFrontPages() + regenerateThreadPage(postid) + +def regenerateFrontPages(): + """ + Regenerates index.html and #.html for each page after that according to the number + of live threads in the database + """ + board = Settings._.BOARD + threads = [] + if board['board_type'] == '1': + threads_to_fetch = int(board['numthreads']) + threads_to_limit = threads_to_fetch + 50 + else: + if board['dir'] == 'o': + threads_to_fetch = threads_to_limit = int(board['numthreads'])*21 + else: + threads_to_fetch = threads_to_limit = int(board['numthreads'])*11 + + database_lock.acquire() + try: + # fetch necessary threads and calculate how many posts we need + allthreads_query = "SELECT id, timestamp, subject, locked, length FROM `posts` WHERE `boardid` = '%s' AND parentid = 0 AND IS_DELETED = 0 ORDER BY `bumped` DESC, `id` ASC LIMIT %d" % \ + (board["id"], threads_to_limit) + allthreads = FetchAll(allthreads_query) + posts_to_fetch = 0 + for t in allthreads[:threads_to_fetch]: + posts_to_fetch += int(t["length"]) + more_threads = allthreads[threads_to_fetch:50] + + # get the needed posts for the front page and order them + posts_query = "SELECT * FROM `posts` WHERE `boardid` = '%s' ORDER BY `bumped` DESC, CASE parentid WHEN 0 THEN id ELSE parentid END ASC, `id` ASC LIMIT %d" % \ + (board["id"], posts_to_fetch) + posts = FetchAll(posts_query) + + threads = [] + if posts: + thread = None + post_num = 0 + + for post in posts: + if post["parentid"] == '0': + skipThread = False + if post["IS_DELETED"] == '0': + # OP; Make new thread + if thread is not None: + thread["length"] = post_num + threads.append(thread) + post_num = post["num"] = 1 + thread = {"id": post["id"], "timestamp": post["timestamp"], "subject": post["subject"], "locked": post["locked"], "posts": [post]} + else: + skipThread = True + else: + if not skipThread: + post_num += 1 + post["num"] = post_num + thread["posts"].append(post) + + if post_num: + thread["length"] = post_num + threads.append(thread) + finally: + database_lock.release() + + pages = [] + is_omitted = False + if len(threads) > 0: + # Todo : Make this better + if board['board_type'] == '1': + page_count = 1 # Front page only + threads_per_page = int(board['numthreads']) + else: + if board['dir'] == 'o': + front_limit = int(board['numthreads'])*21 + else: + front_limit = int(board['numthreads'])*11 + + if len(threads) >= front_limit: + is_omitted = True + + page_count = int(math.ceil(float(len(threads)) / float(int(board['numthreads'])))) + threads_per_page = int(board['numthreads']) + + for i in xrange(page_count): + pages.append([]) + start = i * threads_per_page + end = start + threads_per_page + for thread in threads[start:end]: + pages[i].append(thread) + else: + page_count = 0 + is_omitted = False + pages.append({}) + + page_num = 0 + for pagethreads in pages: + regeneratePage(page_num, page_count, pagethreads, is_omitted, more_threads) + page_num += 1 + +def regeneratePage(page_num, page_count, threads, is_omitted=False, more_threads=[]): + """ + Regenerates a single page and writes it to .html + """ + board = Settings._.BOARD + + for thread in threads: + replylimit = int(board['numcont']) + + # Create reply list + parent = thread["posts"].pop(0) + replies = thread["posts"] + thread["omitted"] = 0 + #thread["omitted_img"] = 0 + + # Omit posts + while(len(replies) > replylimit): + post = replies.pop(0) + thread["omitted"] += 1 + #if post["file"]: + # thread["omitted_img"] += 1 + + # Remake thread with necessary replies only + replies.insert(0, parent) + thread["posts"] = replies + + # Shorten messages + for post in thread["posts"]: + post["shortened"], post["message"] = shortenMsg(post["message"]) + + # Build page according to page number + if page_num == 0: + file_name = "index" + else: + file_name = str(page_num) + + if board['board_type'] == '1': + templatename = "txt_board.html" + else: + templatename = "board.html" + + page_rendered = renderTemplate(templatename, {"threads": threads, "pagenav": pageNavigator(page_num, page_count, is_omitted), "more_threads": more_threads}) + + f = open(Settings.ROOT_DIR + board["dir"] + "/" + file_name + ".html", "w") + try: + f.write(page_rendered) + finally: + f.close() + +def threadList(mode=0): + board = Settings._.BOARD + + if mode == 1: + mobile = True + maxthreads = 20 + cutFactor = 100 + elif mode == 2: + mobile = True + maxthreads = 1000 + cutFactor = 50 + elif mode == 3: + mobile = True + maxthreads = 1000 + cutFactor = 100 + else: + mobile = False + maxthreads = 1000 + cutFactor = 70 + + if board['board_type'] == '1': + filename = "txt_threadlist.html" + full_threads = FetchAll("SELECT id, timestamp, timestamp_formatted, subject, length, last FROM `posts` WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \ + % {'board': board["id"], 'limit': maxthreads}) + else: + filename = "threadlist.html" + full_threads = FetchAll("SELECT p.*, coalesce(x.count,1) AS length, coalesce(x.t,p.timestamp) AS last FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1)+1 as count, max(timestamp) as t FROM `posts` " +\ + "WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s AND p.IS_DELETED = 0 ORDER BY `bumped` DESC LIMIT %(limit)s" \ + % {'board': board["id"], 'limit': maxthreads}) + + # Generate threadlist + timestamps = [] + for thread in full_threads: + if board['board_type'] == '1': + thread["timestamp_formatted"] = thread["timestamp_formatted"].split(" ")[0] + timestamps.append([thread["last"], formatTimestamp(thread["last"])]) + if mobile: + timestamps[-1][1] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", timestamps[-1][1]) + else: + if len(thread['message']) > cutFactor: + thread['shortened'] = True + else: + thread['shortened'] = False + thread['message'] = thread['message'].replace('<br />', ' ') + thread['message'] = thread['message'].split("<hr />")[0] + thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message']) + thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8') + thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities + thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted']) + + # Get last reply if in mobile mode + if mode == 1: + thread['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", thread['timestamp_formatted']) + lastreply = FetchOne("SELECT * FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id'])) + if lastreply: + if len(lastreply['message']) > 60: + lastreply['shortened'] = True + else: + lastreply['shortened'] = False + lastreply['message'] = lastreply['message'].replace('<br />', ' ') + lastreply['message'] = lastreply['message'].split("<hr />")[0] + lastreply['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', lastreply['message']) + lastreply['message'] = lastreply['message'].decode('utf-8')[:60].encode('utf-8') + lastreply['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', lastreply['message']) # Removes incomplete HTML entities + lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted']) + thread["lastreply"] = lastreply + else: + thread["lastreply"] = None + elif mode == 2: + lastreply = FetchOne("SELECT timestamp_formatted FROM `posts` WHERE parentid = %s AND boardid = %s AND IS_DELETED = 0 ORDER BY `timestamp` DESC LIMIT 1" % (thread['id'], board['id'])) + if lastreply: + lastreply['timestamp_formatted'] = re.compile(r"\(.{1,3}\)", re.DOTALL | re.IGNORECASE).sub(" ", lastreply['timestamp_formatted']) + thread["lastreply"] = lastreply + + return renderTemplate(filename, {"more_threads": full_threads, "timestamps": timestamps, "mode": mode}, mobile) + +def catalog(sort=''): + board = Settings._.BOARD + + if board['board_type'] != '0': + raise UserError, "No hay catálogo disponible para esta sección." + + cutFactor = 500 + + q_sort = '`bumped` DESC, `id` ASC' + if sort: + if sort == '1': + q_sort = '`timestamp` DESC' + elif sort == '2': + q_sort = '`timestamp` ASC' + elif sort == '3': + q_sort = '`length` DESC' + elif sort == '4': + q_sort = '`length` ASC' + + threads = FetchAll("SELECT id, subject, message, length, thumb, expires_formatted FROM `posts` " +\ + "WHERE parentid = 0 AND boardid = %(board)s AND IS_DELETED = 0 ORDER BY %(sort)s" \ + % {'board': board["id"], 'sort': q_sort}) + + for thread in threads: + if len(thread['message']) > cutFactor: + thread['shortened'] = True + else: + thread['shortened'] = False + thread['message'] = thread['message'].replace('<br />', ' ') + thread['message'] = thread['message'].split("<hr />")[0] + thread['message'] = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub('', thread['message']) + thread['message'] = thread['message'].decode('utf-8')[:cutFactor].encode('utf-8') + thread['message'] = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub('', thread['message']) # Removes incomplete HTML entities + + return renderTemplate("catalog.html", {"threads": threads, "i_sort": sort}) + +def regenerateThreadPage(postid): + """ + Regenerates /res/#.html for supplied thread id + """ + board = Settings._.BOARD + + thread = getThread(postid) + + if board['board_type'] in ['1', '5']: + template_filename = "txt_thread.html" + outname = Settings.ROOT_DIR + board["dir"] + "/read/" + str(thread["timestamp"]) + ".html" + title_matome = thread['subject'] + post_preview = cut_home_msg(thread['posts'][0]['message'], 0) + else: + template_filename = "board.html" + outname = Settings.ROOT_DIR + board["dir"] + "/res/" + str(postid) + ".html" + post_preview = cut_home_msg(thread['posts'][0]['message'], len(board['name'])) + + if thread['posts'][0]['subject'] != board['subject']: + title_matome = thread['posts'][0]['subject'] + else: + title_matome = post_preview + + page = renderTemplate(template_filename, {"threads": [thread], "replythread": postid, "matome": title_matome, "preview": post_preview}, False) + + f = open(outname, "w") + try: + f.write(page) + finally: + f.close() + +def threadPage(postid, mobile=False, timestamp=0): + board = Settings._.BOARD + + if board['board_type'] in ['1', '5']: + template_filename = "txt_thread.html" + else: + template_filename = "board.html" + + threads = [getThread(postid, mobile, timestamp)] + + return renderTemplate(template_filename, {"threads": threads, "replythread": postid}, mobile) + +def dynamicRead(parentid, ranges, mobile=False): + import re + board = Settings._.BOARD + + if board['board_type'] != '1': + raise UserError, "Esta sección no es un BBS y como tal no soporta lectura dinámica." + + # get entire thread + template_fname = "txt_thread.html" + thread = getThread(timestamp=parentid, mobile=mobile) + + if not thread: + # Try the archive + fname = Settings.ROOT_DIR + board["dir"] + "/kako/" + str(parentid) + ".json" + if os.path.isfile(fname): + import json + with open(fname) as f: + thread = json.load(f) + thread['posts'] = [dict(zip(thread['keys'], row)) for row in thread['posts']] + template_fname = "txt_archive.html" + else: + raise UserError, 'El hilo no existe.' + + filtered_thread = { + "id": thread['id'], + "timestamp": thread['timestamp'], + "length": thread['length'], + "subject": thread['subject'], + "locked": thread['locked'], + "posts": [], + } + + if 'size' in thread: + filtered_thread['size'] = thread['size'] + + no_op = False + if ranges.endswith('n'): + no_op = True + ranges = ranges[:-1] + + # get thread length + total = thread["length"] + + # compile regex + __multiple_ex = re.compile("^([0-9]*)-([0-9]*)$") + __single_ex = re.compile("^([0-9]+)$") + __last_ex = re.compile("^l([0-9]+)$") + start = 0 + end = 0 + + # separate by commas (,) + for range in ranges.split(','): + # single post (#) + range_match = __single_ex.match(range) + if range_match: + postid = int(range_match.group(1)) + if postid > 0 and postid <= total: + filtered_thread["posts"].append(thread["posts"][postid-1]) + + # go to next range + continue + + # post range (#-#) + range_match = __multiple_ex.match(range) + if range_match: + start = int(range_match.group(1) or 1) + end = int(range_match.group(2) or total) + + if start > total: + start = total + if end > total: + end = total + + if start < end: + filtered_thread["posts"].extend(thread["posts"][start-1:end]) + else: + list = thread["posts"][end-1:start] + list.reverse() + filtered_thread["posts"].extend(list) + + # go to next range + continue + + # last posts (l#) + range_match = __last_ex.match(range) + if range_match: + length = int(range_match.group(1)) + start = total - length + 1 + end = total + if start < 1: + start = 1 + + filtered_thread["posts"].extend(thread["posts"][start-1:]) + + continue + + # calculate previous and next ranges + prevrange = None + nextrange = None + if __multiple_ex.match(ranges) or __last_ex.match(ranges): + if mobile: + range_n = 50 + else: + range_n = 100 + + prev_start = start-range_n + prev_end = start-1 + next_start = end+1 + next_end = end+range_n + + if prev_start < 1: + prev_start = 1 + if next_end > total: + next_end = total + + if start > 1: + prevrange = '%d-%d' % (prev_start, prev_end) + if end < total: + nextrange = '%d-%d' % (next_start, next_end) + + if not no_op and start > 1 and end > 1: + filtered_thread["posts"].insert(0, thread["posts"][0]) + + if not filtered_thread["posts"]: + raise UserError, "No hay posts que mostrar." + + post_preview = cut_home_msg(filtered_thread["posts"][0]["message"], 0) + + return renderTemplate(template_fname, {"threads": [filtered_thread], "replythread": parentid, "prevrange": prevrange, "nextrange": nextrange, "preview": post_preview}, mobile, noindex=True) + +def regenerateBoard(everything=False): + """ + Update front pages and every thread res HTML page + """ + board = Settings._.BOARD + op_posts = [] + + if everything: + op_posts = FetchAll("SELECT `id` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0" % board["id"]) + + # Use queues only if multithreading is enabled + if Settings.USE_MULTITHREADING: + request_queue = Queue.Queue() + threads = [RegenerateThread(i, request_queue) for i in range(Settings.MAX_PROGRAM_THREADS)] + for t in threads: + t.start() + + request_queue.put("front") + + for post in op_posts: + request_queue.put(post["id"]) + + for i in range(Settings.MAX_PROGRAM_THREADS): + request_queue.put(None) + + for t in threads: + t.join() + else: + regenerateFrontPages() + for post in op_posts: + regenerateThreadPage(post["id"]) + +def deletePost(postid, password, deltype='0', imageonly=False, quick=False): + """ + Remove post from database and unlink file (if present), along with all replies + if supplied post is a thread + """ + board = Settings._.BOARD + + # make sure postid is numeric + postid = int(postid) + + # get post + post = FetchOne("SELECT `id`, `timestamp`, `parentid`, `file`, `thumb`, `password`, `length` FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(postid))) + + # abort if the post doesn't exist + if not post: + raise UserError, _("There isn't a post with this ID. It was probably deleted.") + + if password: + if password != post['password']: + raise UserError, "No tienes permiso para eliminar este mensaje." + if post["parentid"] == '0' and int(post["length"]) >= Settings.DELETE_FORBID_LENGTH: + raise UserError, "No puedes eliminar un hilo con tantas respuestas." + if (int(time.time()) - int(post["timestamp"])) > 86400: + raise UserError, "No puedes eliminar un post tan viejo." + + # just update the DB if deleting only the image, otherwise delete whole post + if imageonly: + if post["file"]: + deleteFile(post) + + UpdateDb("UPDATE `posts` SET `file` = '', `file_hex` = '', `thumb` = '', `thumb_width` = 0, `thumb_height` = 0 WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], str(post['id']))) + else: + if int(post["parentid"]) == 0: + deleteReplies(post) + + logTime("Deleting post " + str(postid)) + if deltype != '0' and post["parentid"] != '0': + # Soft delete (recycle bin) + UpdateDb("UPDATE `posts` SET `IS_DELETED` = %s WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (deltype, board["id"], post["id"])) + else: + # Hard delete + if post["file"]: + deleteFile(post) + + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], post["id"])) + if post['parentid'] != '0': + UpdateDb("UPDATE `posts` SET length = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (threadNumReplies(post["parentid"]), post["parentid"], board["id"])) + + if post['parentid'] == '0': + if board['board_type'] == '1': + os.unlink(Settings.ROOT_DIR + board["dir"] + "/read/" + post["timestamp"] + ".html") + else: + os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + post["id"] + ".html") + + regenerateHome() + + # rebuild thread and fronts if reply; rebuild only fronts if not + if post["parentid"] != '0': + threadUpdated(post["parentid"]) + else: + regenerateFrontPages() + +def deleteReplies(thread): + board = Settings._.BOARD + + # delete files first + replies = FetchAll("SELECT `parentid`, `file`, `thumb` FROM `posts` WHERE `boardid` = %s AND `parentid` = %s AND `file` != ''" % (board["id"], thread["id"])) + for post in replies: + deleteFile(post) + + # delete all replies from DB + UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `parentid` = %s" % (board["id"], thread["id"])) + +def deleteFile(post): + """ + Unlink file and thumb of supplied post + """ + board = Settings._.BOARD + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/src/" + post["file"]) + except: + pass + + # we don't want to delete mime thumbnails + if post["thumb"].startswith("mime"): + return + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/thumb/" + post["thumb"]) + except: + pass + + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/mobile/" + post["thumb"]) + except: + pass + + if int(post["parentid"]) == 0: + try: + os.unlink(Settings.IMAGES_DIR + board["dir"] + "/cat/" + post["thumb"]) + except: + pass + +def trimThreads(): + """ + Delete any threads which have passed the MAX_THREADS setting + """ + logTime("Trimming threads") + board = Settings._.BOARD + archived = False + + # Use limit of the board type + if board['board_type'] == '1': + limit = Settings.TXT_MAX_THREADS + else: + limit = Settings.MAX_THREADS + + # trim expiring threads first + if board['maxage'] != '0': + t = time.time() + + alert_time = int(round(int(board['maxage']) * Settings.MAX_AGE_ALERT)) + time_limit = t + (alert_time * 86400) + old_ops = FetchAll("SELECT `id`, `timestamp`, `expires`, `expires_alert`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `expires` > 0 AND `expires` < %s LIMIT 50" % (board['id'], time_limit)) + + for op in old_ops: + if t >= int(op['expires']): + # Trim old threads + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(op["id"]) + archived = True + + deletePost(op["id"], None) + else: + # Add alert to threads approaching deletion + UpdateDb("UPDATE `posts` SET expires_alert = 1 WHERE `boardid` = %s AND `id` = %s" % (board['id'], op['id'])) + + # trim inactive threads next + if board['maxinactive'] != '0': + t = time.time() + + oldest_last = t - (int(board['maxinactive']) * 86400) + old_ops = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 AND `last` < %d LIMIT 50" % (board['id'], oldest_last)) + + for op in old_ops: + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(op["id"]) + archived = True + + deletePost(op["id"], None) + + # select trim type by board + if board['board_type'] == '1': + trim_method = Settings.TXT_TRIM_METHOD + else: + trim_method = Settings.TRIM_METHOD + + # select order by trim + if trim_method == 1: + order = 'last DESC' + elif trim_method == 2: + order = 'bumped DESC' + else: + order = 'timestamp DESC' + + # Trim the last thread + op_posts = FetchAll("SELECT `id`, `length` FROM `posts` WHERE `boardid` = %s AND `parentid` = 0 AND IS_DELETED = 0 ORDER BY %s" % (board["id"], order)) + if len(op_posts) > limit: + posts = op_posts[limit:] + for post in posts: + if board['archive'] == '1' and int(op["length"]) >= Settings.ARCHIVE_MIN_LENGTH: + archiveThread(post["id"]) + archived = True + + deletePost(post["id"], None) + pass + + if archived: + regenerateKako() + +def autoclose_thread(parentid, t, replies): + """ + If the thread is crossing the reply limit, close it with a message. + """ + board = Settings._.BOARD + + # decide the replylimit + if board['board_type'] == '1' and Settings.TXT_CLOSE_THREAD_ON_REPLIES > 0: + replylimit = Settings.TXT_CLOSE_THREAD_ON_REPLIES + elif Settings.CLOSE_THREAD_ON_REPLIES > 0: + replylimit = Settings.CLOSE_THREAD_ON_REPLIES + else: + return # do nothing + + # close it if passing replylimit + #if replies >= replylimit or board["dir"] == "polka": + if replies >= replylimit: + notice_post = Post(board["id"]) + notice_post["parentid"] = parentid + notice_post["name"] = "Sistema" + notice_post["message"] = "El hilo ha llegado al límite de respuestas.<br />Si quieres continuarlo, por favor crea otro." + notice_post["timestamp"] = t+1 + notice_post["bumped"] = get_parent_post(parentid, board["id"])["bumped"] + notice_post["timestamp_formatted"] = str(replylimit) + " mensajes" + notice_post.insert() + UpdateDb("UPDATE `posts` SET `locked` = 1 WHERE `boardid` = '%s' AND `id` = '%s' LIMIT 1" % (board["id"], _mysql.escape_string(parentid))) + +def pageNavigator(page_num, page_count, is_omitted=False): + """ + Create page navigator in the format of [0], [1], [2]... + """ + board = Settings._.BOARD + + # No threads? + if page_count == 0: + return '' + + # TODO nijigen HACK + first_str = "Primera página" + last_str = "Última página" + previous_str = _("Previous") + next_str = _("Next") + omitted_str = "Resto omitido" + + pagenav = "<span>" + if page_num == 0: + pagenav += first_str + else: + previous = str(page_num - 1) + if previous == "0": + previous = "" + else: + previous = previous + ".html" + pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + previous + '"><input value="'+previous_str+'" type="submit" /></form>' + + pagenav += "</span><span>" + + for i in xrange(page_count): + if i == page_num: + pagenav += "[<strong>%d</strong>]" % i + else: + if i == 0: + pagenav += '[<a href="%s%s/">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i) + else: + pagenav += '[<a href="%s%s/%d.html">%d</a>]' % (Settings.BOARDS_URL, board['dir'], i, i) + + if i > 0 and (i % 10) == 0 and not is_omitted: + pagenav += '<br />' + elif i < 10: + pagenav += ' ' + + if is_omitted: + pagenav += "[" + omitted_str + "]" + + pagenav += "<!-- "+repr(is_omitted)+"-->" + pagenav += "</span><span>" + + next = (page_num + 1) + if next == page_count: + pagenav += last_str + "</span>" + else: + pagenav += '<form method="get" action="' + Settings.BOARDS_URL + board["dir"] + '/' + str(next) + '.html"><input value="'+next_str+'" type="submit" /></form></span>' + + return pagenav + +def flood_check(t,post,boardid): + board = Settings._.BOARD + + if not post["parentid"]: + maxtime = t - int(board['threadsecs']) + #lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0) + + # NO MATTER THE IP + lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (boardid, maxtime), 0) + pass + else: + maxtime = t - int(board['postsecs']) + lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = '%s' and `parentid` != 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, maxtime), 0) + + if int(lastpost[0]): + if post["parentid"]: + raise UserError, _("Flood detected. Please wait a moment before posting again.") + else: + lastpost = FetchOne("SELECT `timestamp` FROM `posts` WHERE `parentid`=0 and `boardid`='%s' and IS_DELETED = 0 ORDER BY `timestamp` DESC" % (boardid), 0) + wait = int(int(board['threadsecs']) - (t - int(lastpost[0]))) + raise UserError, "Por favor espera " + str(wait) + " segundos antes de crear otro hilo." + +def cut_home_msg(message, boardlength=0): + short_message = message.replace("<br />", " ") + short_message = short_message.split("<hr />")[0] + short_message = re.compile(r"<[^>]*?>", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes HTML tags + limit = Settings.HOME_LASTPOSTS_LENGTH - boardlength + + if len(short_message) > limit: + if isinstance(short_message, unicode): + short_message = short_message[:limit].encode('utf-8') + "…" + else: + short_message = short_message.decode('utf-8')[:limit].encode('utf-8') + "…" + short_message = re.compile(r"&(.(?!;))*$", re.DOTALL | re.IGNORECASE).sub("", short_message) # Removes incomplete HTML + return short_message + +def getLastAge(limit): + threads = [] + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, bumped, last, length, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND posts.locked < 3 ORDER BY bumped DESC LIMIT %d" % limit + threads = FetchAll(sql) + + for post in threads: + post['id'] = int(post['id']) + post['bumped'] = int(post['bumped']) + post['last'] = int(post['last']) + post['length'] = int(post['length']) + post['board_type'] = int(post['board_type']) + post['timestamp'] = int(post['timestamp']) + post['content'] = cut_home_msg(post['content'], 0) + + if post['board_type'] == 1: + post['url'] = '/%s/read/%d/l10' % (post['dir'], post['timestamp']) + else: + post['url'] = '/%s/res/%d.html' % (post['dir'], post['id']) + + return threads + +def getNewThreads(limit): + threads = [] + sql = "SELECT posts.id, boards.name AS board_fulln, boards.subname AS board_name, board_type, boards.dir, timestamp, thumb, CASE WHEN posts.subject = boards.subject THEN posts.message ELSE posts.subject END AS content FROM posts INNER JOIN boards ON boardid = boards.id WHERE parentid = 0 AND IS_DELETED = 0 AND boards.secret = 0 AND boards.id <> 34 AND boards.id <> 13 AND posts.locked = 0 ORDER BY timestamp DESC LIMIT %d" % (limit) + threads = FetchAll(sql) + + for post in threads: + post['id'] = int(post['id']) + post['board_type'] = int(post['board_type']) + post['timestamp'] = int(post['timestamp']) + post['timestamp_formatted'] = formatTimestamp(post['timestamp'], True) + post['timestamp_formatted'] = post['timestamp_formatted'][:8] + ' ' + post['timestamp_formatted'][13:] + post['content'] = cut_home_msg(post['content'], 0) + if post['board_type'] == 1: + post['url'] = '/%s/read/%d' % (post['dir'], post['timestamp']) + else: + post['url'] = '/%s/res/%d.html' % (post['dir'], post['id']) + + return threads + +def regenerateHome(): + """ + Update index.html in the boards directory with useful data for users + """ + logTime("Updating home") + t = datetime.datetime.now() + + limit = Settings.HOME_LASTPOSTS + template_values = { + 'header': Settings.SITE_TITLE, + 'slogan': Settings.SITE_SLOGAN, + 'latest_news': FetchAll("SELECT `timestamp`, `message`, `timestamp_formatted` FROM `news` WHERE `type` = '2' ORDER BY `timestamp` DESC LIMIT " + str(Settings.HOME_NEWS)), + 'latest_age': getLastAge(limit), + 'latest_age_num': limit, + 'new_threads': getNewThreads(Settings.HOME_NEWTHREADS), + } + + page_rendered = renderTemplate('home.html', template_values) + f = open(Settings.HOME_DIR + "home.html", "w") + try: + f.write(page_rendered) + finally: + f.close() + + if Settings.ENABLE_RSS: + sql = "SELECT id, boardid, board_name, timestamp, timestamp_formatted, content, url FROM last ORDER BY timestamp DESC LIMIT 10" + rss = FetchAll(sql) + rss_rendered = renderTemplate('home.rss', {'posts': rss}) + f = open(Settings.HOME_DIR + "bai.rss", "w") + try: + f.write(rss_rendered) + finally: + f.close() + +def regenerateNews(): + """ + Update news.html in the boards directory with older news + """ + posts = FetchAll("SELECT * FROM `news` WHERE `type` = '1' ORDER BY `timestamp` DESC") + template_values = { + 'title': 'Noticias', + 'posts': posts, + 'header': Settings.SITE_TITLE, + 'slogan': Settings.SITE_SLOGAN, + 'navbar': False, + } + + page_rendered = renderTemplate('news.html', template_values) + + f = open(Settings.HOME_DIR + "noticias.html", "w") + try: + f.write(page_rendered) + finally: + f.close() + +def regenerateAccess(): + if not Settings.HTACCESS_GEN: + return False + + bans = FetchAll("SELECT INET_NTOA(`ip`) AS 'ip', INET_NTOA(`netmask`) AS 'netmask', `boards` FROM `bans` WHERE `blind` = '1'") + listbans = dict() + #listbans_global = list() + + boarddirs = FetchAll('SELECT `dir` FROM `boards`') + for board in boarddirs: + listbans[board['dir']] = list() + + for ban in bans: + ipmask = ban["ip"] + if ban["netmask"] is not None: + ipmask += '/' + ban["netmask"] + + if ban["boards"] != "": + boards = pickle.loads(ban["boards"]) + for board in boards: + listbans[board].append(ipmask) + else: + #listbans_global.append(ban["ip"]) + for board in boarddirs: + if board['dir'] not in Settings.EXCLUDE_GLOBAL_BANS: + listbans[board['dir']].append(ipmask) + + # Generate .htaccess for each board + for board in listbans.keys(): + template_values = { + 'ips': listbans[board], + 'dir': board, + } + + page_rendered = renderTemplate('htaccess', template_values) + f = open(Settings.ROOT_DIR + board + "/.htaccess", "w") + try: + f.write(page_rendered) + finally: + f.close() + + return True + +def regenerateKako(): + board = Settings._.BOARD + + threads = FetchAll("SELECT * FROM archive WHERE boardid = %s ORDER BY timestamp DESC" % board['id']) + page = renderTemplate('kako.html', {'threads': threads}) + with open(Settings.ROOT_DIR + board["dir"] + "/kako/index.html", "w") as f: + f.write(page) + +def make_url(postid, post, parent_post, noko, mobile): + board = Settings._.BOARD + + parentid = post["parentid"] + if not parentid: + parentid = postid + + if mobile: + if not noko: + url = Settings.CGI_URL + 'mobile/' + board["dir"] + elif board["board_type"] == '1': + url = "%s/mobileread/%s/%s/l10#form" % (Settings.CGI_URL, board["dir"], parent_post['timestamp']) + else: + url = "%s/mobileread/%s/%s#%s" % (Settings.CGI_URL, board["dir"], parentid, postid) + else: + if not noko: + url = Settings.BOARDS_URL + board["dir"] + "/" + elif board["board_type"] == '1': + url = "%s/read/%s/l50#bottom" % (Settings.BOARDS_URL + board["dir"], str(parent_post['timestamp'])) + else: + url = "%s/res/%s.html#%s" % (Settings.BOARDS_URL + board["dir"], str(parentid), postid) + + return url + +def make_redirect(url, timetaken=None): + board = Settings._.BOARD + randomPhrase = getRandomLine('quotes.conf') + + return renderTemplate('redirect.html', {'url': url, 'message': randomPhrase, 'timetaken': timetaken}) + +def latestAdd(post, postnum, postid, parent_post): + board = Settings._.BOARD + + #UpdateDb("DELETE FROM last LIMIT 15, 500#") + + if post['subject'] and post['subject'] != board["subject"]: + content = post['subject'] + else: + content = cut_home_msg(post['message'], len(board['name'])) + + timestamp_formatted = datetime.datetime.fromtimestamp(post['timestamp']).strftime('%Y-%m-%dT%H:%M:%S%Z') + parentid = parent_post['id'] if post['parentid'] else postid + + if board['board_type'] == '1': + url = '/%s/read/%s/%d' % (board['dir'], (parent_post['timestamp'] if post['parentid'] else post['timestamp']), (postnum if postnum else 1)) + else: + url = '/%s/res/%s.html#%s' % (board['dir'], parentid, postid) + + sql = "INSERT INTO last (id, boardid, board_name, timestamp, timestamp_formatted, content, url) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (str(postid), board['id'], _mysql.escape_string(board['name']), post['timestamp'], _mysql.escape_string(timestamp_formatted), _mysql.escape_string(content), _mysql.escape_string(url)) + UpdateDb(sql) + +def latestRemove(postid): + board = Settings._.BOARD + UpdateDb("DELETE FROM last WHERE id = %s AND boardid = %s" % (str(postid), board['id'])) + +def archiveThread(postid): + import json + board = Settings._.BOARD + + thread = getThread(postid, False) + post_preview = cut_home_msg(thread['posts'][0]['message'], 0) + + page = renderTemplate("txt_archive.html", {"threads": [thread], "replythread": postid, "preview": post_preview}, False) + with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".html", "w") as f: + f.write(page) + + thread['keys'] = ['num', 'IS_DELETED', 'name', 'tripcode', 'email', 'message', 'timestamp_formatted'] + thread['posts'] = [[row[key] for key in thread['keys']] for row in thread['posts']] + try: + with open(Settings.ROOT_DIR + board["dir"] + "/kako/" + str(thread['timestamp']) + ".json", "w") as f: + json.dump(thread, f, indent=0) + except: + raise UserError, "Can't archive: %s" % thread['timestamp'] + + UpdateDb("REPLACE INTO archive (id, boardid, timestamp, subject, length) VALUES ('%s', '%s', '%s', '%s', '%s')" % (thread['id'], board['id'], thread['timestamp'], _mysql.escape_string(thread['subject']), thread['length'])) + +def throw_dice(dice): + qty = int(dice[0][1:]) + if qty == 0: + raise UserError, "No tienes dados para lanzar." + if qty > 100: + qty = 100 + sides = int(dice[1][1:]) if dice[1] else 6 + if sides == 0: + raise UserError, "Tus dados no tienen caras." + if sides > 100: + sides = 100 + + string = "Lanzas " + string += "un dado de " if qty == 1 else (str(qty) + " dados de ") + string += "una cara." if sides == 1 else (str(sides) + " caras.") + string += " Resultado: <b>" + + total = i = 0 + while (i < qty): + total += random.randint(1,sides) + i += 1 + + string += str(total) + "</b>" + + return string + +def magic_ball(): + string = "La bola 8 mágica dice: <b>" + results = ["Sí.", "Es seguro.", "En mi opinión, sí.", "Está decidido que sí.", "Definitivamente sí.", "Es lo más probable.", "Buen pronóstico.", "Puedes confiar en ello.", "Todo apunta a que sí.", "Sin duda.", "Respuesta vaga, vuelve a intentarlo.", "Pregunta en otro momento.", "Mejor que no te lo diga ahora.", "No puedo predecirlo ahora.", "Concéntrate y vuelve a preguntar.", "No cuentes con ello.", "Mi respuesta es no.", "Mis fuentes me dicen que no.", "Pronóstico no muy bueno.", "Muy dudoso."] + string += random.choice(results) + "</b>" + + return string + +def discord_hook(post, url): + import urllib2 + import json + + board = Settings._.BOARD + + WEBHOOK_URL = "https://discordapp.com/api/webhooks/428025764974166018/msYu1-R3JRnG-cxrhAu3J7LbIPvzpBlJwbW5PFe5VEQaxVzjros9CXOpjZDahUE42Jgn" + + data = {"content": "", + "ts": post['timestamp'], + "embeds": [{ + "title": post['subject'], + "description": cut_home_msg(post['message'], 30), + "url": "https://bienvenidoainternet.org" + url, + "color": 11910504, + "timestamp": datetime.datetime.utcfromtimestamp(post['timestamp']).isoformat(), + "footer": { "text": board['name'] }, + "thumbnail": { "url": "https://bienvenidoainternet.org/%s/thumb/%s" % (board['dir'], post['thumb']) }, + "author": { + "name": "Nuevo hilo", + "icon_url": "https://bienvenidoainternet.org/0/junk/w/shobon.gif" + }}] + } + jsondata = json.dumps(data, separators=(',',':')) + + opener = urllib2.build_opener() + opener.addheaders = [('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0')] + response = opener.open(WEBHOOK_URL, jsondata, 6) + the_page = response.read() |