# coding=utf-8
import math
import os
import shutil
import time
import threading
import Queue
import _mysql
import formatting
import logging
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):
logging.info("Insertando Post")
post_values = []
for key, value in self.post.iteritems():
if key == 'ip':
template = "INET6_ATON('%s')"
else:
template = "'%s'"
post_values.append(template % _mysql.escape_string(str(value)))
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, 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, 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"] + "/res/" + 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"
thread = getThread(postid, mobile, timestamp)
if not thread:
raise UserError, "El hilo no existe."
return renderTemplate(template_filename, {"threads": [thread], "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."
elif post["parentid"] == '0' and int(post["length"]) >= Settings.DELETE_FORBID_LENGTH:
raise UserError, "No puedes eliminar un hilo con tantas respuestas."
elif (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)
logging.info("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)
if post['parentid'] != '0':
numreplies = threadNumReplies(post["parentid"])
if numreplies > 2:
newlast = FetchOne('SELECT timestamp, email FROM posts WHERE boardid = %s AND parentid = %s AND timestamp < %d ORDER BY timestamp DESC LIMIT 1' % (board['id'], post['parentid'], int(post['timestamp'])))
else:
newlast = FetchOne('SELECT timestamp FROM posts WHERE boardid = %s AND id = %s LIMIT 1' % (board['id'], post['parentid']))
UpdateDb("DELETE FROM `posts` WHERE `boardid` = %s AND `id` = %s LIMIT 1" % (board["id"], post["id"]))
if post['parentid'] != '0':
UpdateDb("UPDATE `posts` SET last = %s, length = %d WHERE `id` = '%s' AND `boardid` = '%s'" % (newlast["timestamp"], threadNumReplies(post["parentid"]), post["parentid"], board["id"]))
if post['parentid'] == '0':
if board['board_type'] == '1':
os.unlink(Settings.ROOT_DIR + board["dir"] + "/res/" + 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
"""
logging.debug("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 = round(t - int(board['threadsecs']))
#lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = INET6_ATON('%s') and `parentid` = 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, int(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, int(maxtime)), 0)
else:
maxtime = round(t - int(board['postsecs']))
lastpost = FetchOne("SELECT COUNT(*) FROM `posts` WHERE `ip` = INET6_ATON('%s') and `parentid` != 0 and `boardid` = '%s' and IS_DELETED = 0 AND timestamp > %d" % (str(post["ip"]), boardid, int(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, "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(board_type, 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 AND boards.board_type = %d ORDER BY bumped DESC LIMIT %d" % (board_type, 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
"""
logging.debug("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_ib': getLastAge(0, limit),
'latest_age_bbs': getLastAge(1, 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 `ipstr`, `boards` FROM `bans` WHERE `blind` = '1' ORDER BY `ipstart` ASC")
boards = FetchAll('SELECT `dir` FROM `boards`')
global_boards = [board['dir'] for board in boards if board['dir'] not in Settings.EXCLUDE_GLOBAL_BANS]
global_bans = []
board_bans = {}
for ban in bans:
if ban["boards"]:
boards = pickle.loads(ban["boards"])
for board in boards:
board_bans.setdefault(board, []).append(ban["ipstr"])
else:
global_bans.append(ban["ipstr"])
# Generate .htaccess for each board
""" TODO: htaccess
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()"""
template_values = {
'global_boards': global_boards,
'global_bans': global_bans,
'board_bans': board_bans,
}
page_rendered = renderTemplate('bans_geo', template_values)
with open(Settings.ROOT_DIR + "/bans_geo.conf", "w") as f:
f.write(page_rendered)
page_rendered = renderTemplate('bans_locations', template_values)
with open(Settings.ROOT_DIR + "/bans_locations.conf", "w") as f:
f.write(page_rendered)
os.utime(Settings.ROOT_DIR + "/bans.conf", None)
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):
if not Settings.DISCORD_HOOK_URL:
return
import urllib2
import json
board = Settings._.BOARD
#data = {"embeds": [{
# "title": post['subject'],
# "description": cut_home_msg(post['message'], 30),
# "url": "https://bienvenidoainternet.org" + url, # TODO: Parametrizar.
# "color": 11910504,
# "timestamp": datetime.datetime.utcfromtimestamp(post['timestamp']).isoformat(),
# "footer": { "text": board['name'] },
# "thumbnail": { "url": "%s%s/thumb/%s" % (Settings.HOME_URL, board['dir'], post['thumb']) },
# "author": {
# "name": "Nuevo hilo",
# "icon_url": "%s0/junk/w/shobon.gif" % Settings.HOME_URL
# }}]
#}
data = {"content": "test"}
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(Settings.DISCORD_HOOK_URL, jsondata, 6)
the_page = response.read()