#coding:utf-8
import functools,os,time,gc,random,re
repattern=re.compile('voting_token" value="(.*?)"')

portList=tuple([i for i in range(55568,55579)])#本服务器监听端口

#验证码服务器
captchaServers=[
    'http://localhost:8889',
    ]
random.shuffle(captchaServers)
class CaptchaServers:
    captchaServers=captchaServers
captchaServers=tuple(captchaServers)#随机切换服务器的排列顺序，
#避免多个Voter经常向同一服务器post

def captcha_server_generator():#每调用一次next(csGen)，产生一个captchaServers中的url
    length=len(captchaServers)
    i=0
    while 1:
        yield captchaServers[i]
        i+=1
        if(i>=length):
            i=0
            
csGen=captcha_server_generator()

from charaSelector import selector

#超时设置
request_timeout=50
captcha_timeout=60

#异步服务，且客户端不需要返回值
#实战中的功能：收取一批ip地址的post，然后对每次post开一个线程投票
import tornado.ioloop
import tornado.web
from concurrent.futures import ThreadPoolExecutor
import requests,cfscrape

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

from hashlib import md5,sha256

#f=open('./tmp.txt','a',encoding='utf-8')

from fake_useragent import UserAgent
uaGen = UserAgent(
    fallback="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0"
    )
acceptLanguage=(
    'zh-cn,zh;q=0.8,zh-tw;q=0.7,zh-hk;q=0.5,en-us;q=0.3',
    'zh-cn,zh;q=0.8,zh-tw;q=0.7,zh-hk;q=0.5',
    'ar-sa;q=0.9,en;q=0.5',
    'ru;q=0.8,en;q=0.7',
    'en-us;q=0.8,en;q=0.5',
    'en;q=0.8',
    )
indexLanguage=len(acceptLanguage) - 1
def genHeaders():#aiohttp会自动生成大部分header
    headers = {
        'user-agent': uaGen.random,
        'accept': "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
        'referer': "https://www.internationalsaimoe.com/voting/",
        'accept-encoding': "gzip, deflate",
        'accept-language': acceptLanguage[random.randint(0, indexLanguage)],
        'cache-control': "no-cache",
        'connection':'keep-alive',
        'X-Requested-With':'XMLHttpRequest',
        'Upgrade-Insecure-Requests':'1',
    }
    return headers
localsession=cfscrape.create_scraper(headers=genHeaders())

from io import BytesIO
from PIL import Image
import numpy as np
from skimage.filters import gaussian
from skimage.exposure import equalize_hist
from skimage.morphology import opening,label
from skimage.measure import regionprops
def judge(img): #判断能否识别验证码
    img = img[97:,:]
    img=gaussian(img, sigma=0.85)
    img=equalize_hist(img)
    img=(img > 0.7)*1.0
    img=opening(img,selem=np.array([[0,1,0],[1,1,1],[0,1,0]]))
    image_region=img*0
    img=label(img,connectivity=1)
    props = regionprops(img)
    cnt=0
    centerx=[]
    width=[]
    for i in range(np.max(img)):
        if props[i]['area'] > 290:
            image_region = image_region + (img==props[i]['label'])
            centerx.append(props[i]['centroid'][1])
            width.append(props[i]['image'].shape[1])
            cnt += 1
    # 八个连通域
    if cnt != 8:
        return False
    # 连通域之间距离；宽高比
    centerx.sort()
    if np.std(np.diff(np.array(centerx))) > 10 or np.min(np.array(width)) < 18:
        return False
    return True

class NoVotingToken(Exception):
    '''自定义异常类。没找到voting_token时raise NoVotingToken'''
    def __init__(self):#, length, atleast):
        pass
        #self.length = length
        #self.atleast = atleast

RequestExceptions=(
    requests.RequestException,
    requests.ConnectionError,
    requests.HTTPError,
    requests.Timeout,
    )

from retryapi import retry,RetryExhausted

#这个函数在class MainHandler里也有一份，可在线程池运行

#def localsession_get(url='https://coinone.co.kr/'):#珂以测试防火墙
def localsession_get(url="https://www.internationalsaimoe.com"):
    with localsession.get(url,timeout=request_timeout,verify=False) as res:
        print('IgnaleoMT:本地session请求%s，状态码为%d'%(url,res.status_code))
        #print(res.text)

worker_loop=tornado.ioloop.IOLoop.instance()
class MainHandler(tornado.web.RequestHandler):
    id=1
    executor = ThreadPoolExecutor(30)

    class VoterMT:
        def __init__(self,proxy,loop,localsession,executor,id=0):
            self.proxy=proxy
            self.loop=loop
            self.session=cfscrape.create_scraper(headers=genHeaders())
            self.localsession=localsession
            self.executor=executor
            self.id=id #用于标记这次投票是第几次
            #if proxy:
            self.fingerprint=md5((proxy+'ChNeWi').encode()).hexdigest()
            #else:#仅用于测试！
            #    self.fingerprint=md5(('Hecate2'+str(time.time())).encode()).hexdigest()
            #    self.voting_token=sha256(('Hecate2'+str(time.time())).encode()).hexdigest()
            #self.localsession=localsession

        @retry(exceptions=RequestExceptions,tries=5,logger=None)
        def _get(self,url,timeout=request_timeout):
            with self.session.get(url,timeout=timeout,proxies={"http":self.proxy,"https":self.proxy}) as response:
                if (response.status_code<400):
                    if 'text' in response.headers['content-type']:
                        #f=open('./tmp.txt','a',encoding='utf-8')
                        #f.write(text)
                        #f.close()
                        return response.text
                    #if 'image' in response.content_type:
                    else:
                        #fb=open('./tmp.png','wb')
                        #fb.write(body)
                        #fb.close()
                        return response.content
                #if (response.status==503):
                    #pass
                    #处理cloudflare防火墙
                else:
                    return response.raise_for_status()

        #不使用代理，直接get
        #暂时不要对着世萌用这个。如果一开始没有墙，突然墙开起来了，可能会出问题
        #因为同一个本地session会发起几百几千个get，吃到不同的墙
        @retry(exceptions=RequestExceptions,tries=5,logger=None)
        def _localget(self,url,timeout=request_timeout):
            with self.session.get(url,timeout=timeout) as response:
                if (response.status_code<400):
                    if 'text' in response.headers['content-type']:
                        #f=open('./tmp.txt','a',encoding='utf-8')
                        #f.write(text)
                        #f.close()
                        return response.text
                    #if 'image' in response.content_type:
                    else:
                        #fb=open('./tmp.png','wb')
                        #fb.write(body)
                        #fb.close()
                        return response.content
                #if (response.status==503):
                    #pass
                    #处理cloudflare防火墙
                else:
                    return response.raise_for_status()
                
        @retry(exceptions=RequestExceptions,tries=5,logger=None)
        def _post(self,url,data,timeout=request_timeout):
            with self.session.post(url,data=data,timeout=timeout,proxies={"http":self.proxy,"https":self.proxy}) as response:
                if (response.status_code<400):
                    return response.text
                else:
                    return response.raise_for_status()
        
        @retry(exceptions=RequestExceptions,tries=5,logger=None)
        def _localpost(self,url,data,timeout=request_timeout):
            with self.localsession.post(url,data=data,timeout=timeout) as response:
                if (response.status_code<400):
                    return response.text
                else:
                    return response.raise_for_status()

        def EnterISML(self):
            text=self._get('https://www.internationalsaimoe.com/voting')
            voting_token = re.search(repattern, text)
            if voting_token:
                self.html=text
                self.voting_token=voting_token.group(1)
                self.startTime=time.time()
                print(self.id,self.proxy,'进入ISML成功')
            else:
                print(self.id,self.proxy,'找不到voting_token')
                raise NoVotingToken

        #发指纹
        def PostFingerprint(self):#确保self.fingerprint在class初始化时已经生成！
            self._post("https://www.internationalsaimoe.com/security",data={"secure":self.fingerprint})
            print(self.id,self.proxy,'发指纹成功')
            return('%d %s 发指纹成功'%(self.id,self.proxy))

        def AIDeCaptcha(self):
            #打码。包含多次下载验证码，预处理，以及交给服务器最终识别
            tries=0
            while 1:#while tries<重试上限:
            #目前验证码重试次数不设上限！
                tries+=1
                raw_img=self._localget(
                    'https://www.internationalsaimoe.com/captcha/%s/%s' % (self.voting_token, int(time.time() * 1000)),
                    timeout=captcha_timeout)
                img=Image.open(BytesIO(raw_img))
                img = 255-np.array(img.convert('L') ) #转化为灰度图
                if(judge(img)):
                    del img
                    print(self.id,self.proxy,'第%d次获取验证码，能够识别'%(tries))
                    captcha=self._localpost(next(csGen),raw_img)
                    self.captcha=captcha
                    return captcha
        def DeCaptcha(self):
            #打码。直接丢给打码平台处理
            #captcha=await self._get()
            #self.captcha='打码结果'
            print('丢给打码平台的版本还未完成！')
            raise

        def Submit(self):#提交投票
            postdata=selector(self.html,self.voting_token,self.captcha)
            sleepTime=190-(time.time()-self.startTime)#消耗的时间减去90秒
            if(sleepTime>0):#还没到90秒
                print(self.id,self.proxy,"开始等待%d秒"%(sleepTime))
                time.sleep(sleepTime)#坐等到90秒
            result=self._post("https://www.internationalsaimoe.com/voting/submit",data=postdata)
            return result

        def SaveHTML(self):#存票根
            text=self._get('https://www.internationalsaimoe.com/voting')
            try:
                f=open('./HTML/%s.html'%(self.captcha),'w',encoding=('utf-8'))
                f.write(text)
                f.close()
                return('%d %s 存票根成功'%(self.id,self.proxy))
            except Exception:
                return('%d %s 由于硬盘原因，存票根失败，可能硬盘过载!!!!!'%(self.id,self.proxy))

        @tornado.concurrent.run_on_executor
        def Vote(self):#跑完整个投票流程！建议由Launch函数启动
            try:
                self.EnterISML()
            except NoVotingToken:
                self.session.close()
                return('%d %s %s'%(self.id,self.proxy,'找不到voting_token'))
                #self.voting_token=sha256(('Hecate2'+str(time.time())).encode()).hexdigest()
            except RetryExhausted:
                self.session.close()
                return('%d %s %s'%(self.id,self.proxy,'连续重试次数超限'))
            try:
                #下面开始发指纹
                self.PostFingerprint()
                #下面开始识别验证码任务。
                self.AIDeCaptcha()
                #下面坐等到90秒然后submit
                result=self.Submit()
                print('%d %s %s'%(self.id,self.proxy,result))
                #下面应对验证码错误
                if('Invalid' in result):#验证码错误
                    self.AIDeCaptcha()
                    result=self.Submit()
                    print('%d %s %s'%(self.id,self.proxy,result))
                #下面存票根
                if('successful' in result):
                    result=self.SaveHTML()
                    self.PostFingerprint()
                    self.session.close()
                    return result
                #if('refresh' in result): #session expired
                #    print('%d %s %s'%(self.id,self.proxy,result))
                    #if(random.random()<0.6):
                    #    self.Vote() #再来一次！
                #if('An entry' in result): #这个ip被抢先投票
                self.session.close()
                return('%d %s %s'%(self.id,self.proxy,result)) #结束投票
            except RetryExhausted:
                self.session.close()
                return('%d %s %s'%(self.id,self.proxy,'连续重试次数超限'))

    @tornado.concurrent.run_on_executor
    def localsession_get(self,url="https://www.internationalsaimoe.com"):
        with localsession.get(url,timeout=request_timeout) as res:
            self.write('IgnaleoMT:本地session请求%s，状态码为%d'%(url,res.status_code))
            print('IgnaleoMT:本地session请求%s，状态码为%d'%(url,res.status_code))

    #@tornado.concurrent.run_on_executor
    def get(self, *args, **kwargs):
        gc.collect()
        worker_loop.add_callback(functools.partial(self.localsession_get))
        for server in CaptchaServers.captchaServers:
            worker_loop.add_callback(functools.partial(self.localsession_get, url=server))

    @tornado.concurrent.run_on_executor
    def post(self):
        #gc.collect()
        proxies=self.request.body.decode(encoding='utf-8').split('\r\n')
        self.write('收到POST')
        print('Ignaleo: 收到POST')
        #print('收到POST\n',proxies)
        for proxy in proxies:
            self.id+=1
            voter=self.VoterMT('http://'+proxy,worker_loop,localsession,self.executor,id=self.id)
            #voter=VoterMT(None,worker_loop,localsession,id=self.id)
            #不使用代理，仅用于测试！
            worker_loop.add_callback(functools.partial(voter.Vote))

def run_proc(port):
    app=tornado.web.Application([
        (r'/',MainHandler),
    ])
    app.listen(port)
    print('DestroyerIgnaleoMT@localhost:%d'%(port))
    
    print('Ignaleo开始准备本地session')
    #本地session，可用于向验证码服务器post，或许还可以用来取验证码
    worker_loop.add_callback(functools.partial(localsession_get))
    for server in CaptchaServers.captchaServers:
        worker_loop.add_callback(functools.partial(localsession_get, url=server))
    worker_loop.start()

if __name__ == '__main__':
    from multiprocessing import Process
    length=len(portList)
    for port in range(length-1):
        p=Process(target=run_proc, args=(portList[port],))
        p.start()
    run_proc(portList[length-1])
