aboutsummaryrefslogblamecommitdiff
path: root/cgi/weabot.py
blob: 86c86fe632028f66007a383fce6fc8d19ca688a6 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                      








                                                               
              

                                     











                             
                      







                                                                        

                                                
 
                                                                                                                            
 
                              
                                                                                     













                                                                      





                                         
             

                          
                                        
                                   
                                     

                                       







                                                                       
                  



                                          
                                                 




                                
                                        
                         
                                      













                                                                                                                              
             












                                                                                              
                                                             








                                                                

                    



                                                         

                                                                                           






                                                   
                                                                   

                                                     
                            













                                                                  
                                                    


                                                                                                     
                                                                                                          







                                                              
                             
                        







                                                          





                                                                                                                                                              











                                                 


                                                                                                                                  





                                               
                                            
                                                                        






















                                                                            

                                                  






                                                            





                                                            
                                                                                                                                     









                                                                   
                                                                                             



                                               

                                                        
                                                                                                    
                                                                                                                 
 
                                                                                      
                                                                                      

                                                                         
                     
                                                                      




















                                                                        
                            
















                                                                        
                                                                                                                                                                         


                                                                                                                                                          
                     
                                                                                      
                                                                                                                                                                

                                           
                                                                                                                      

                                    
                                         

                                                                        



                                                                            
                                                    
                                            









                                                                       
                                              
                                                      
















                                                                                                                                                                     

                                                            
                                                                                                        
                                                                                                                     
 
                                                                                




                                                            
                                                                                                       
                                         
                                             





                                                                                                                                                                                           

                 
                                                                                                                                                            
                                                                
 
                                                                                                                         










                                                             

                                                   
                                                               
                                                                 

                       
                



                                  

                                
                                                               
                           
                                                               
                              
                                                               
                               
                                                                
                                    
                                                                        


                          
                                                            




                                                             
                                                                                                         



                                                

                                                                                                         

                                                                 
                                                            
                                                                                                         
                                
                                                                             


                                
                       










                                                              
                                          
                                                                                 




                                         
                                     
                                            
 
                                                      
 
                                        








                                                               
                                  


                                                                         
                                                                           






                                              






                                                                

                                                               
                                                                      









                                                                    
                                           



                                                                       
                                                                               




                              




                                                                   












                                                                     
                                  

                                 
                              




                                      
                                      
                                         
                                      
                                           
                                      
                                           
                                      


                                      
                                     
                                         
                                     
                                            
                                     
                                             
                                     
                                             
                                            
                                         

                                            









                                                   
                          
                                                                            
                                                                                                  

                                    
                                  

                                                               
                                    
                                                            

                                                                  
                                   
                                                             
             
                                                                  

                                                                          
                                                 

                                                                           


                                       

                                                                
             

                                                        
















                                                                                                 
                                                                   


                                                             
                                                                    










                                                                             
                         


                      
                                       









































                                                                                                                       
                                                     






                                                 
                                       
                            

                                                                          







                                                                                                
                                  

                                





                                                                       
                                                                                  


                                                                       

                                            
                                                                                 





                                                               
                                  










                                                             
                                     


                                            
                                                


                                                      
                                                        





                                            

                                                                











                                                                        
                                                                                                                                                 








                                                                                               






                                                                             

                                                  
                                                             

                                                                                  

                                                                                      

                                                                               
                                        





                                                              
                                                                                  

                                                                                                                  




                                                                

                                                                                                    









                                           

                                                                    



                                                                            
                               
                               
                                            
                                                                                   
                                                         


                                         


                                                                  
                                                               



                                                        
                                                                    



                      
                                                 
                             
                                                







                                  

                                                                      

                              
                                                                                                                                                     
                    
                                                    

                       
                                    





                                                                                                                                                  
             
                                                                                                    


                       
                                                                     

                                                 
                                                                                                                              
                                                                      
                                                                                                                    





                                                                       
            

                                              
                                               









                                           
                                                                                                                                                                                                     
                                              
                                                                                                            
                                                                           
                                                                           
 



                                                                                                                                 
 

                                                                              

                     

                                                                  
 
                                                                                          


                       




                                                                                                                                   






















                                                                                                                                                                                                                                        
                                             


























                                                                             

                          
                                           



                                                                 
                                              







                                         
 


                                                        
#!/usr/bin/env python3
# coding=utf-8

# Remove the first line to use the env command to locate python

import os
import time
import datetime
import random
import cgi
import logging
from MySQLdb import _mysql
from http.cookies import SimpleCookie

import tenjin
import manage
import oekaki
import gettext
from database import *
from settings import Settings
from framework import *
from formatting import *
from post import *
from img import *

__version__ = "0.10.9"

# Set to True to disable weabot's exception routing and enable profiling
_DEBUG = False

# Set to True to save performance data to weabot.txt
_LOG = False

class weabot(object):
    def __init__(self, environ, start_response):
        global _DEBUG

        logging.basicConfig(filename='weabot.log', format='%(asctime)s %(levelname)s %(message)s', level=Settings.LOG_LEVEL)

        self.environ = environ
        self.ip = self.environ.get("HTTP_X_REAL_IP", self.environ.get("REMOTE_ADDR"))
        if self.environ["PATH_INFO"].startswith("/weabot.py/"):
            self.environ["PATH_INFO"] = self.environ["PATH_INFO"][11:]

        self.start = start_response
        self.formdata = getFormData(self)
        self.output = ""

        self.handleRequest()

        # Localization Code
        lang = gettext.translation(
            'weabot', './locale', languages=[Settings.LANG])
        lang.install()

        if _DEBUG:
            import cProfile

            prof = cProfile.Profile()
            prof.runcall(self.run)
            prof.dump_stats('stats.prof')
        else:
            try:
                self.run()
            except UserError as message:
                self.error(message)
            except Exception as inst:
                logging.exception(inst)

                import sys
                import traceback
                exc_type, exc_value, exc_traceback = sys.exc_info()
                detail = ((os.path.basename(o[0]), o[1], o[2], o[3])
                          for o in traceback.extract_tb(exc_traceback))
                self.exception(type(inst), inst, detail)

        # close database and finish
        #CloseDb()

    def __iter__(self):
        self.handleResponse()
        self.start("200 OK", self.headers)
        self.output = self.output.encode('utf-8')
        yield self.output

    def error(self, message):
        board = Settings._.BOARD
        if board:
            if board['board_type'] == 1:
                info = {}
                info['host'] = self.ip
                info['name'] = self.formdata.get('fielda', '')
                info['email'] = self.formdata.get('fieldb', '')
                info['message'] = self.formdata.get('message', '')

                self.output += renderTemplate("txt_error.html",
                                              {"info": info, "error": message})
            else:
                mobile = self.formdata.get('mobile', '')
                if mobile:
                    self.output += renderTemplate("mobile/error.html",
                                                  {"error": message})
                else:
                    self.output += renderTemplate("error.html", {
                                                  "error": message, "boards_url": Settings.BOARDS_URL, "board": board["dir"]})
        else:
            self.output += renderTemplate("exception.html",
                                          {"exception": None, "error": message})

    def exception(self, type, message, detail):
        self.output += renderTemplate("exception.html",
                                      {"exception": type, "error": message, "detail": detail})

    def handleRequest(self):
        self.headers = [("Content-Type", "text/html")]
        self.handleCookies()

    def handleResponse(self):
        if self._newcookies:
            for newcookie in list(self._newcookies.values()):
                self.headers.append(
                    ("Set-Cookie", newcookie.output(header="")))

    def handleCookies(self):
        self._cookies = SimpleCookie()
        self._cookies.load(self.environ.get("HTTP_COOKIE", ""))
        self._newcookies = None

    def run(self):
        clearCache()

        path_split = self.environ["PATH_INFO"].split("/")
        caught = False

        if Settings.FULL_MAINTENANCE:
            raise UserError(_(
                "%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE)

        if len(path_split) > 1:
            if path_split[1] == "post":
                # Making a post
                caught = True

                if 'password' not in self.formdata:
                    raise UserError("El request está incompleto.")

                # let's get all the POST data we need
                ip = self.ip
                boarddir = self.formdata.get('board')
                parent = self.formdata.get('parent')
                trap1 = self.formdata.get('name', '')
                trap2 = self.formdata.get('email', '')
                name = self.formdata.get('fielda', '')
                email = self.formdata.get('fieldb', '')
                subject = self.formdata.get('subject', '')
                message = self.formdata.get('message', '')
                file = self.formdata.get('file')
                file_original = self.formdata.get('file_original')
                spoil = self.formdata.get('spoil')
                oek_file = self.formdata.get('oek_file')
                password = self.formdata.get('password', '')
                noimage = self.formdata.get('noimage')
                mobile = ("mobile" in self.formdata)

                # call post function
                (post_url, ttaken, unused) = self.make_post(ip, boarddir, parent, trap1, trap2, name,
                 email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile)

                # make redirect
                self.output += make_redirect(post_url, ttaken)
            elif path_split[1] == "environ":
                caught = True

                self.output += repr(self.environ)
            elif path_split[1] == "delete":
                caught = True
                OpenDb()

                boarddir = self.formdata.get('board')
                postid = self.formdata.get('delete')
                imageonly = self.formdata.get('imageonly')
                password = self.formdata.get('password')
                mobile = self.formdata.get('mobile')

                # call delete function
                deletePosts(boarddir, postid, imageonly, password)

                self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s/" /><p>%s</p></body></html>' % (
                    ("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + boarddir,
                    ("Archivo" if imageonly else "Post") + " eliminado con éxito."
                  )
            elif path_split[1] == "anarkia":
                import anarkia
                caught = True
                OpenDb()
                anarkia.anarkia(self, path_split)
            elif path_split[1] == "manage":
                caught = True
                OpenDb()
                manage.manage(self, path_split)
            elif path_split[1] == "api":
                import api
                caught = True
                self.headers = [("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*"),
                  ("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"),
                  ("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With")]
                OpenDb()
                api.api(self, path_split)
            elif path_split[1] == "threadlist":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                if board['board_type'] != 1:
                    raise UserError("No disponible para esta sección.")
                self.output = threadList(0)
            elif path_split[1] == "mobile":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                self.output = threadList(1)
            elif path_split[1] == "mobilelist":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                self.output = threadList(2)
            elif path_split[1] == "mobilecat":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                self.output = threadList(3)
            elif path_split[1] == "mobilenew":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                self.output = renderTemplate('txt_newthread.html', {}, True)
            elif path_split[1] == "mobilehome":
                OpenDb()
                latest_age_ib = getLastAge(0, 10)
                latest_age_bbs = getLastAge(1, 10)
                for threads in latest_age_ib:
                    content = threads['url']
                    content = content.replace('/read/', '/')
                    content = content.replace('/res/', '/')
                    content = content.replace('.html', '')
                    threads['url'] = content
                for threads in latest_age_bbs:
                    content = threads['url']
                    content = content.replace('/read/', '/')
                    content = content.replace('/res/', '/')
                    content = content.replace('.html', '')
                    threads['url'] = content
                caught = True
                self.output = renderTemplate('latest.html', {'latest_age_ib': latest_age_ib, 'latest_age_bbs': latest_age_bbs}, True)
            elif path_split[1] == "mobilenewest":
                OpenDb()
                newthreads = getNewThreads(Settings.HOME_LASTPOSTS)
                for threads in newthreads:
                    content = threads['url']
                    content = content.replace('/read/', '/')
                    content = content.replace('/res/', '/')
                    content = content.replace('.html', '')
                    threads['url'] = content
                caught = True
                self.output = renderTemplate('newest.html', {'newthreads': newthreads}, True)
            elif path_split[1] == "mobileread":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True

                # Redirect to ban page if user is banned
                if Settings.ENABLE_BANS and addressIsBanned(self.ip, board["dir"], blind_only=True):
                    raise UserError('<meta http-equiv="refresh" content="0; url=/cgi/banned/%s">' % board["dir"])
 
                if len(path_split) > 4 and path_split[4] and board['board_type'] == 1:
                    self.output = dynamicRead(int(path_split[3]), path_split[4], True)
                elif board['board_type'] == 1:
                    self.output = threadPage(0, True, int(path_split[3]))
                else:
                    self.output = threadPage(int(path_split[3]), True)
            elif path_split[1] == "catalog":
                OpenDb()
                board = setBoard(path_split[2])
                caught = True
                sort = self.formdata.get('sort', '')
                self.output = catalog(sort)
            elif path_split[1] == "oekaki":
                caught = True
                OpenDb()
                oekaki.oekaki(self, path_split)
            elif path_split[1] == "play":
                # Module player
                caught = True
                boarddir = path_split[2]
                modfile = path_split[3]
                self.output = renderTemplate(
                    'mod.html', {'board': boarddir, 'modfile': modfile})
            elif path_split[1] == "report":
                # Report post, check if they are enabled
                # Can't report if banned
                caught = True
                ip = self.ip
                boarddir = path_split[2]
                postid = int(path_split[3])
                reason = self.formdata.get('reason')
                try:
                    txt = True
                    postshow = int(path_split[4])
                except:
                    txt = False
                    postshow = postid

                self.report(ip, boarddir, postid, reason, txt, postshow)
            elif path_split[1] == "stats":
                caught = True
                self.stats()
            elif path_split[1] == "random":
                caught = True
                OpenDb()
                board = FetchOne("SELECT `id`, `dir`, `board_type` FROM `boards` WHERE `secret` = 0 AND `id` <> 1 AND `id` <> 13 AND `id` <> 34 ORDER BY RAND() LIMIT 1")
                thread = FetchOne("SELECT `id`, `timestamp` FROM `posts` WHERE `parentid` = 0 AND `boardid` = %s ORDER BY RAND() LIMIT 1", (board['id'],))
                if board['board_type'] == 1:
                    url = f"{Settings.HOME_URL}{board['dir']}/read/{thread['timestamp']}/"
                else:
                    url = f"{Settings.HOME_URL}{board['dir']}/res/{thread['id']}.html"
                self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><meta http-equiv="refresh" content="0;url=%s" /><body><p>...</p></body></html>' % url
            elif path_split[1] == "banned":
                OpenDb()
                bans = FetchAll("SELECT * FROM `bans` WHERE INET6_ATON(%s) BETWEEN `ipstart` AND `ipend`", (self.ip,))
                if bans:
                    for ban in bans:
                        if ban["boards"]:
                            boards = str2boards(ban["boards"])
                        if not ban["boards"] or path_split[2] in boards:
                            caught = True
                            if ban["boards"]:
                                boards_str = '/' + '/, /'.join(boards) + '/'
                            else:
                                boards_str = 'todas'
                            if ban["until"]:
                                expire = formatTimestamp(ban["until"])
                            else:
                                expire = ""

                            template_values = {
                                # 'return_board': path_split[2],
                                'boards_str': boards_str,
                                'reason': ban['reason'],
                                'added': formatTimestamp(ban["added"]),
                                'expire': expire,
                                'ip': self.ip,
                                'ipstr': ban['ipstr'],
                            }
                            self.output = renderTemplate(
                                'banned.html', template_values)
                else:
                    if len(path_split) > 2:
                        caught = True
                        self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>%s</p></body></html>' % (
                            Settings.HOME_URL + path_split[2], _("Your ban has expired. Redirecting..."))
            elif path_split[1] == "read":
                # Textboard read:
                if len(path_split) > 4:
                    caught = True
                    # 2: board
                    # 3: thread
                    # 4: post(s)
                    OpenDb()
                    board = setBoard(path_split[2])

                    # Redirect to ban page if user is banned
                    if Settings.ENABLE_BANS and addressIsBanned(self.ip, board["dir"], blind_only=True):
                        raise UserError('<meta http-equiv="refresh" content="0; url=/cgi/banned/%s">' % board["dir"])
 
                    self.output = dynamicRead(int(path_split[3]), path_split[4])
            elif path_split[1] == "preview":
                caught = True
                OpenDb()
                try:
                    board = setBoard(self.formdata["board"])
                    message = format_post(self.formdata["message"], self.ip, self.formdata["parentid"])
                    self.output = message
                except Exception as messagez:
                    self.output = "Error: " + \
                        str(messagez) + " : " + str(self.formdata)
        if not caught:
            # Redirect the user back to the front page
            self.output += '<html xmlns="http://www.w3.org/1999/xhtml"><body><meta http-equiv="refresh" content="0;url=%s" /><p>--&gt; --&gt; --&gt;</p></body></html>' % Settings.HOME_URL

        CloseDb()

    def make_post(self, ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile):
        _STARTTIME = time.process_time()  # Comment if not debug

        if Settings.PROXY_BANS and ip not in Settings.PROXY_WHITELIST and (boarddir not in Settings.EXCLUDE_GLOBAL_BANS):
            if addressIsTor(ip):
                logging.warn("Proxy: Tor %s" % ip)
                raise UserError("Proxy prohibido.")
            if addressIsProxy(ip):
                logging.warn("Proxy: Proxy %s" % ip)
                raise UserError("Proxy prohibido.")
            if addressIsBannedCountry(ip):
                logging.warn("Proxy: Banned country %s" % ip)
                raise UserError("Proxy prohibido.")
            if not addressIsES(ip):
                logging.warn("Proxy: Not ES %s" % ip)
                raise UserError("Proxy prohibido.")
            if hostIsBanned(ip):
                logging.warn("Proxy: Blacklisted host %s" % ip)
                raise UserError("Sufijo de host en lista negra.")

        # open database
        OpenDb()

        # set the board
        board = setBoard(boarddir)

        # check length of fields
        if len(name) > 50:
            raise UserError("El campo de nombre es muy largo.")
        if len(email) > 50:
            raise UserError("El campo de e-mail es muy largo.")
        if len(subject) > 100:
            raise UserError("El campo de asunto es muy largo.")
        if len(message) > 8000:
            raise UserError("El campo de mensaje es muy largo.")
        if message.count('\n') > 99:
            raise UserError("El mensaje tiene muchos saltos de línea.")

        # anti-spam trap
        if trap1 or trap2:
            raise UserError("Te quedan tres días de vida.")

        # Create a single datetime now so everything syncs up
        t = time.time()

        # Delete expired bans
        deletedBans = UpdateDb("DELETE FROM `bans` WHERE `until` != 0 AND `until` < " + str(timestamp()))
        if deletedBans > 0:
            regenerateAccess()

        # Redirect to ban page if user is banned
        if Settings.ENABLE_BANS and addressIsBanned(ip, board["dir"]):
            raise UserError('<meta http-equiv="refresh" content="0; url=/cgi/banned/%s">' % board["dir"])

        # Disallow posting if the site OR board is in maintenance
        if Settings.MAINTENANCE and board["dir"] != 'polka':
            raise UserError(_("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE)
        if board["locked"] == 1:
            raise UserError(_("This board is closed. You can't post in it."))

        # create post object
        post = Post(board["id"])
        post["ip"] = ip
        post["timestamp"] = post["bumped"] = int(t)
        post["timestamp_formatted"] = formatTimestamp(t)

        # load parent info if we are replying
        parent_post = None
        parent_timestamp = post["timestamp"]
        if parent:
            parent_post = get_parent_post(parent, board["id"])
            parent_timestamp = parent_post['timestamp']
            post["parentid"] = parent_post['id']
            post["bumped"] = parent_post['bumped']
            if parent_post['locked'] == 1:
                raise UserError(_("The thread is closed. You can't post in it."))

        # check if the user is flooding
        flood_check(t, post, board["id"])

        # use fields only if enabled
        if not board["disable_name"]:
            post["name"] = cleanString(name)

        post["email"] = cleanString(email, quote=True)

        if not board["disable_subject"]:
            post["subject"] = cleanString(subject)

        # process tripcodes
        post["name"], post["tripcode"] = tripcode(post["name"])

        # Remove carriage return, they're useless
        message = message.replace("\r", "")

        # check ! functions before
        extend = extend_str = None

        if not post["parentid"] and board["dir"] not in ['bai', 'world']:
            # creating thread
            __extend = re.compile(r"!extend(:\w+)(:\w+)?\n", re.IGNORECASE)
            res = __extend.match(message)
            if res:
                extend = res.groups()
                # truncate extend
                extend_str = res.group(0)
                message = message[res.end(0):]

        # use and format message
        if message.strip():
            post["message"] = format_post(
                message, ip, post["parentid"], parent_timestamp)

        # add function messages
        if extend_str:
            extend_str = extend_str[1:]
            extend_str = extend_str.replace('extend', 'EXTEND')
            post["message"] += '<hr />' + extend_str + ' configurado.'

        if not post["parentid"] and post["email"].lower() == 'sage':
            post["email"] = ""

        # disallow illegal characters
        if post["name"]:
            post["name"] = post["name"].replace('★', '☆')
            post["name"] = post["name"].replace('◆', '◇')

        # process capcodes
        cap_id = hide_end = use_icon = None
        if post["name"] in Settings.CAPCODES:
            capcode = Settings.CAPCODES[post["name"]]
            if post["tripcode"] == (Settings.TRIP_CHAR + capcode[0]):
                post["name"], post["tripcode"] = capcode[1], capcode[2]
                cap_id, hide_end, use_icon = capcode[3], capcode[4], capcode[5]

        # hide ip if necessary
        if hide_end:
            post["ip"] = 0

        # insert icon
        if use_icon:
            img_src = '<img src="%s" alt="ico" /><br />' % use_icon
            post["message"] = img_src + post["message"]

        # use password
        post["password"] = password

        # EXTEND feature
        if post["parentid"] and board["dir"] not in ['bai', 'world']:
            # replying
            __extend = re.compile(r"<hr />EXTEND(:\w+)(:\w+)?\b")
            res = __extend.search(parent_post["message"])
            if res:
                extend = res.groups()

            # compatibility : old id function
            if 'id' in parent_post["email"]:
                board["useid"] = 3

        if 'id' in post["email"]:
            board["useid"] = 3

        if extend:
            try:
                # 1: ID
                if extend[0] == ':no':
                    board["useid"] = 0
                elif extend[0] == ':yes':
                    board["useid"] = 1
                elif extend[0] == ':force':
                    board["useid"] = 2
                elif extend[0] == ':extra':
                    board["useid"] = 3

                # 2: Slip
                if extend[1] == ':no':
                    board["slip"] = 0
                elif extend[1] == ':yes':
                    board["slip"] = 1
                elif extend[1] == ':domain':
                    board["slip"] = 2
                elif extend[1] == ':verbose':
                    board["slip"] = 3
                elif extend[1] == ':country':
                    board["countrycode"] = 1
                elif extend[1] == ':all':
                    board["slip"] = 3
                    board["countrycode"] = 1
            except IndexError:
                pass

        # if we are replying, use first post's time
        if post["parentid"]:
            tim = parent_post["timestamp"]
        else:
            tim = post["timestamp"]

        # make ID hash
        if board["useid"]:
            post["posterid"] = iphash(ip, post, tim, board["useid"], mobile,
              self.environ["HTTP_USER_AGENT"], cap_id, hide_end, (board["countrycode"] in [1, 2]))

        # use for future file checks
        xfile = (file or oek_file)

        # textboard inforcements (change it to settings maybe?)
        if board['board_type'] == 1:
            if not post["parentid"] and not post["subject"]:
                raise UserError(_(
                    "You must enter a title to create a thread."))
            if not post["message"]:
                raise UserError(_("Please enter a message."))
        else:
            if not post["parentid"] and not xfile and not noimage:
                raise UserError(_(
                    "You must upload an image first to create a thread."))
            if not xfile and not post["message"]:
                raise UserError(_(
                    "Please enter a message or upload an image to reply."))

        # check if this post is allowed
        if post["parentid"]:
            if file and not board['allow_image_replies']:
                raise UserError(_("Image replies not allowed."))
        else:
            if file and not board['allow_images']:
                raise UserError(_("No images allowed."))

        # use default values when missing / remove sage from wrong fields
        if (not post["name"] and not post["tripcode"]) or (post["name"].lower() == 'sage'):
            post["name"] = random.choice(board["anonymous"].split('|'))
        if (not post["subject"] and not post["parentid"]) or (post["subject"].lower() == 'sage'):
            post["subject"] = board["subject"]
        if not post["message"]:
            post["message"] = board["message"]

        # process files
        if oek_file:
            try:
                fname = os.path.join(Settings.IMAGES_DIR,
                                     board['dir'], "temp", oek_file + ".png")
                with open(fname, 'rb') as f:
                    file = f.read()
            except:
                raise UserError("Imposible leer la imagen oekaki.")

        if file and not noimage:
            post = processImage(post, file, t, file_original,
                                (spoil and board['allow_spoilers']))

        if oek_file:
            # Remove temporary oekaki file if everything went right
            # os.remove(fname)
            # TODO: We will rename the file for now. We don't want lost work.
            try:
                os.rename(fname, fname + ".bak")
            except:
                pass  # Just keep it if anything went wrong

        # slip
        if board["slip"]:
            slips = []

            # name
            if board["slip"] in [1, 3]:
                if time.strftime("%H") in ['00', '24'] and time.strftime("%M") == '00' and time.strftime("%S") == '00':
                    host_nick = '000000'
                else:
                    host_nick = 'sarin'

                    if hide_end:
                        host_nick = '★'
                    elif addressIsTor(ip):
                        host_nick = 'onion'
                    else:
                        isps = {'cablevision': 'easy',
                                'cantv':       'warrior',
                                'claro':       'america',
                                'cnet':        'nova',
                                'copelnet':    'cisneros',
                                'cps.com':     'silver',
                                'cybercable':  'bricklayer',
                                'entel':       'matte',
                                'eternet':     'stream',
                                'fibertel':    'roughage',
                                'geonet':      'thunder',
                                'gtdinternet': 'casanueva',
                                'ifxnw':       'effect',
                                'infinitum':   'telegraph',
                                'intercable':  'easy',
                                'intercity':   'cordoba',
                                'iplannet':    'conquest',
                                'itcsa.net':   'sarmiento',
                                'megared':     'clear',
                                'movistar':    'bell',
                                'nextel':      'fleet',
                                'speedy':      'oxygen',
                                'telecom':     'license',
                                'telmex':      'slender',
                                'telnor':      'compass',
                                'tie.cl':      'bell',
                                'vtr.net':     'liberty',
                                'utfsm':       'virgin',
                                }
                        host = getHost(ip)

                        if host:
                            for k, v in isps.items():
                                if k in host:
                                    host_nick = v
                                    break

                slips.append(host_nick)

            # hash
            if board["slip"] in [1, 3]:
                if hide_end:
                    slips.append(
                        '-'.join((getMD5(ip + post["name"])[:4], '****')))
                elif addressIsTor(ip):
                    slips.append(
                        '-'.join(('****', getMD5(self.environ["HTTP_USER_AGENT"])[:4])))
                else:
                    slips.append(
                        '-'.join((getMD5(ip)[:4], getMD5(self.environ["HTTP_USER_AGENT"])[:4])))

            # host
            if board["slip"] == 2:
                if hide_end:
                    host = '★'
                else:
                    host = getHost(ip)
                    if host:
                        hosts = host.split('.')
                        if len(hosts) > 2:
                            if hosts[-2] in ['ne', 'net', 'com', 'co']:
                                host = '.'.join((hosts[-3], hosts[-2], hosts[-1]))
                            else:
                                host = '.'.join((hosts[-2], hosts[-1]))
                            host = '*.' + host
                    elif ':' in ip:
                        iprs = ip.split(':')
                        host = '%s:%s:%s:*:*.*.*.*' % (iprs[0], iprs[1], iprs[2])
                    else:
                        iprs = ip.split('.')
                        host = '%s.%s.*.*' % (iprs[0], iprs[1])
                slips.append(host)

            # IP
            if board["slip"] == 3:
                if hide_end:
                    host = '[*.*.*.*]'
                else:
                    iprs = ip.split('.')
                    host = '[%s.%s.*.*]' % (iprs[0], iprs[1])
                slips.append(host)

            if slips:
                post["tripcode"] += " (%s)" % ' '.join(slips)

        # country code
        if board["countrycode"] == 1:
            if hide_end or addressIsTor(ip):
                country = '??'
            else:
                country = getCountry(ip) or '??'
            post["name"] += " <em>[%s]</em>" % country

        # set expiration date if necessary
        if board["maxage"] > 0 and not post["parentid"]:
            if board["dir"] == '2d':
                date_format = '%m月%d日'
                date_format_y = '%Y年%m月'
            else:
                date_format = '%d/%m'
                date_format_y = '%m/%Y'
            post["expires"] = int(t) + (board["maxage"] * 86400)
            if board["maxage"] >= 365:
                date_format = date_format_y
            post["expires_formatted"] = datetime.datetime.fromtimestamp(
                post["expires"]).strftime(date_format)

        if not post["parentid"]:
            # fill with default values if creating a new thread
            post["length"] = 1
            post["last"] = post["timestamp"]

            if board["dir"] == 'noticias':
                # check if there's at least one link
                if "<a href" not in post["message"]:
                    raise UserError("Al momento de crear un hilo en esta sección necesitas incluír al menos 1 link como fuente en tu mensaje.")

                # insert icon if needed
                img_src = '<img src="%s" alt="ico" /><br />' % getRandomIco()
                post["message"] = img_src + post["message"]

        # insert post, then run timThreads to make sure the board doesn't exceed the page limit
        postid = post.insert()

        # delete threads that have crossed last page
        trimmed = trimThreads()
        
        # let's stop here if the thread we posted in got trimmed
        if post["parentid"] and post["parentid"] in trimmed:
            regenerateFrontPages()
            regenerateHome()
            raise UserError("El hilo en el que publicaste ya fue eliminado.")

        # fix null references when creating thread
        if board["board_type"] == 1 and not post["parentid"]:
            post["message"] = re.compile(r'<a href="/(\w+)/res/0.html/(.+)"').sub(
                r'<a href="/\1/res/'+str(postid)+r'.html/\2"', post["message"])
            UpdateDb("UPDATE `posts` SET message = %s WHERE boardid = %s AND id = %s",
                (post["message"], board["id"], postid))

        # do operations if replying to a thread (bump, autoclose, update cache)
        logging.debug("Updating thread")
        thread_length = None
        if post["parentid"]:
            # get length of the thread
            thread_length = threadNumReplies(post["parentid"])

            # bump if not saged
            if 'sage' not in post["email"].lower() and parent_post['locked'] != 2:
                UpdateDb("UPDATE `posts` SET bumped = %s WHERE (`id` = %s OR `parentid` = %s) AND `boardid` = %s",
                        (post["timestamp"], post["parentid"], post["parentid"], board["id"]))

            # check if thread must be closed
            autoclose_thread(post["parentid"], t, thread_length)

            # update final attributes (length and last post)
            UpdateDb("UPDATE `posts` SET length = %s, last = %s WHERE `id` = %s AND `boardid` = %s",
                    (thread_length, post["timestamp"], post["parentid"], board["id"]))

            # update cache
            threadUpdated(post["parentid"])
        else:
            # create cache for new thread
            threadUpdated(postid)

        regenerateHome()

        # make page redirect
        ttaken = timeTaken(_STARTTIME, time.process_time())
        noko = 'noko' in email.lower() or (board["board_type"] == 1)

        # get new post url
        post_url = make_url(postid, post, parent_post or post, noko, mobile)

        if not board['secret']:
            # call discord hook
            if Settings.ENABLE_DISCORD_HOOK:
                hook_url = make_url(postid, post, parent_post or post, True, False)
                discord_hook(post, parent_post, hook_url)

        return (post_url, ttaken, postid)

    def report(self, ip, boarddir, postid, reason, txt, postshow):
        # don't allow if the report system is off
        if not Settings.REPORTS_ENABLE:
            raise UserError(_('Report system is deactivated.'))

        # if there's not a reason, show the report page
        if reason is None:
            self.output += renderTemplate("report.html",
              {'finished': False, 'postshow': postshow, 'txt': txt})
            return

        # check reason
        if not reason:
            raise UserError(_("Enter a reason."))
        if len(reason) > 100:
            raise UserError(_("Text too long."))

        # open database
        OpenDb()

        # set the board we're in
        board = setBoard(boarddir)

        # check if he's banned
        if Settings.ENABLE_BANS and addressIsBanned(ip, board["dir"]):
            raise UserError(_("You're banned."))

        # check if post exists
        post = FetchOne("SELECT `id`, `parentid`, `ip`, `timestamp` FROM `posts` WHERE `id` = %s AND `boardid` = %s LIMIT 1", (postid, board['id']) )
        if not post:
           raise UserError(_("Post doesn't exist."))

        # generate link
        if board["board_type"] == 1:
            if post['parentid']:
                parent_post = FetchOne("SELECT id, timestamp FROM posts WHERE id = %s AND boardid = %s LIMIT 1", (post["parentid"], board["id"]) )
            else:
                parent_post = post

            link = "/%s/read/%s/%s" % (board["dir"], parent_post["timestamp"], postshow)
        else:
            link = "/%s/res/%s.html#%s" % (board["dir"], post["parentid"] or post["id"], post["id"])

        # insert report
        t = time.time()
        message = html.escape(self.formdata["reason"]).strip()[0:800]
        message = message.replace("\n", "<br />")

        UpdateDb("INSERT INTO `reports` (board, postid, parentid, link, ip, reason, repip, timestamp, timestamp_formatted) " +
            "VALUES (%s, %s, %s, %s, %s, %s, INET6_ATON(%s), %s, %s)",
            (board["dir"], post['id'], post['parentid'], link, post['ip'], message, self.ip, t, formatTimestamp(t)))
        self.output = renderTemplate("report.html", {'finished': True})

    def stats(self):
        import json
        import math
        import platform
        try:
            with open('stats.json', 'r') as f:
                out = json.load(f)
        except (ValueError, FileNotFoundError):
            out = {'t': 0}

        regenerated = False
        if (time.time() - out['t']) > 3600:
            regenerated = True

            # open database
            OpenDb()

            # 1 week = 604800
            query_day = FetchAll("SELECT DATE_FORMAT(FROM_UNIXTIME(FLOOR((timestamp-10800)/86400)*86400+86400), \"%Y-%m-%d\") AS date, COUNT(1) AS count, COUNT(IF(parentid=0, 1, NULL)) AS threads "
                                 "FROM posts "
                                 "WHERE (timestamp-10800) > (UNIX_TIMESTAMP()-604800) AND (IS_DELETED = 0) "
                                 "GROUP BY FLOOR((timestamp-10800)/86400) "
                                 "ORDER BY FLOOR((timestamp-10800)/86400)")

            query_count = FetchOne("SELECT COUNT(1) AS posts, COUNT(NULLIF(file, '')) AS files, VERSION() AS version FROM posts")
            total = query_count["posts"]
            total_files = query_count["files"]
            mysql_ver = query_count["version"]

            archive_count = FetchOne("SELECT SUM(length) AS sum FROM archive")
            total_archived = int(archive_count["sum"])

            days = []
            for r in query_day[1:]:
                days.append((r["date"], r["count"], r["threads"]))

            query_b = FetchAll("SELECT id, dir, name FROM boards WHERE boards.secret = 0")

            boards = []
            totalp = 0
            for b in query_b:
                bposts = FetchOne("SELECT COUNT(1) AS count FROM posts "
                                  "WHERE posts.boardid = %s AND timestamp > ( UNIX_TIMESTAMP(DATE(NOW())) - 2419200 )", (b['id'],))
                boards.append((b['dir'], b['name'], bposts["count"]))
                totalp += bposts["count"]

            boards = sorted(boards, key=lambda boards: boards[2], reverse=True)

            boards_percent = []
            for dir, longname, bposts in boards:
                if bposts > 0:
                    boards_percent.append((dir, longname, '{0:.2f}'.format(
                        float(bposts)*100/totalp), int(bposts)))
                else:
                    boards_percent.append((dir, longname, '0.00', '0'))

            #posts = FetchAll("SELECT `parentid`, `boardid` FROM `posts` INNER JOIN `boards` ON posts.boardid = boards.id WHERE posts.parentid<>0 AND posts.timestamp>(UNIX_TIMESTAMP()-86400) AND boards.secret=0 ORDER BY `parentid`")
            #threads = {}
            # for post in posts:
            #  if post["parentid"] in threads:
            #    threads[post["parentid"]] += 1
            #  else:
            #    threads[post["parentid"]] = 1

            python_version = platform.python_version()
            if self.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'):
                python_version += " (CGI)"
            else:
                python_version += " (bjoern)"

            out = {
                "uname": platform.uname(),
                "python_ver": python_version,
                "python_impl": platform.python_implementation(),
                "python_build": platform.python_build()[1],
                "python_compiler": platform.python_compiler(),
                "mysql_ver": mysql_ver,
                "tenjin_ver": tenjin.__version__,
                "weabot_ver": __version__,
                "days": days,
                "boards": boards,
                "boards_percent": boards_percent,
                "total": total,
                "total_files": total_files,
                "total_archived": total_archived,
                "t": timestamp(),
                "tz": Settings.TIME_ZONE,
            }
            with open('stats.json', 'w') as f:
                json.dump(out, f)

        out['timestamp'] = re.sub(r"\(...\)", " ", formatTimestamp(out['t']))
        out['regenerated'] = regenerated
        self.output = renderTemplate("stats.html", out)
        #self.headers = [("Content-Type", "application/json")]


if __name__ == "__main__":
    from flup.server.fcgi import WSGIServer

    # Psyco is not required, however it will be used if available
    try:
        import psyco
        logging.debug("Psyco se ha instalado")
        psyco.bind(tenjin.helpers.to_str)
        psyco.bind(weabot.run, 2)
        psyco.bind(getFormData)
        psyco.bind(setCookie)
        psyco.bind(threadUpdated)
        psyco.bind(processImage)
    except:
        pass

    app = WSGIServer(weabot, debug=True, forceCGI=False)
    app.run()