 #!/usr/bin/env python
# -*- coding: UTF-8 -*-

# CTerm Control script
# Requires: CTerm 3.271 Build 2007-10-29 or later
# Author: nullspace(jxhchina@gmail.com)
# You may freely modify or redistribute this file and related files
# Please retain version information

# 更新历史：
# 15:40 2008-1-9
# 改进email的识别，支持形如user+tag@gmail.com的email
#
# 0:42 2008-1-4
# 支持形如72.43.]的IP(mitbbs)
#
# 12:05 2007-11-30
# 状态识别和url解析支持可变term行列数
#
# 14:20 04-10-7
# epathch增加@, urlexp1增加中的用户名部分@，长度增至30
# 不知是否会出问题...
# 
# 20:36 04-9-25
# 状态识别，可用正则表达式
# 子状态改用字典；可设多条(行，关键字)规则
# 改这些只是为了判断水木与白云的离站选单
#
# 14:17 04-9-24
# URL按照bat的建议，分析顶层域名，排除了大多数文件名
# 允许断线自动重连(OnSessionClose)
# 
# 6:58 04-9-2
# 状态识别OnGetStatus, OnGetMessage
# 启动、关闭事件OnCTermStart, OnCTermClose
#
# 10:37 04-9-1
# 改进了跨行URL识别，支持未名上某些跨行但行末带有1到数个空格(设为最多3个)的URL
# 支持白云黄鹤上中文文件名后仍有一串字符的URL
# 
# 14:48 04-8-31
# 实现了系统脚本与其它脚本线程之间的通信
# 提供了dataEvents，当有数据到来时被set，随即clear
# 在其它脚本或事件函数中可以wait相应的dataEvents[ID]
# 
# 4:04 04-8-29
# OnParseURL:
# 跨行URL识别，最多识别3行
# 
# 22:38 04-8-27
# OnParseURL:
# 识别www，ftp，bbs等字样
# 将url，proxy，email分开，更条理、清晰些，也应该更准确，范围更广些
#
# 10:23 04-8-26
# OnParseURL：
# 支持中文URL，排除全数字非IP的URL，可识别出扩展名, 代理email，自动添加协议头
# 界定有效搜索范围，理论上提高了识别的效率，特别是对空格等无效字符，一测试即返回
#
# 9:20 04-8-26
# 提供了基本的连接、数据、键盘、鼠标事件
# 一个实用的功能：解析URL
# 
# 9:17 2007-10-29
# OnKeyDown带返回值
# URL解析增加bAutoHeader参数
# 

from CTerm import *
import string, re, threading, thread
import datetime, time
#from runpy import run_module

#print 'in ct_system.py'

import sys
sys.path.insert(0, GetWorkDir() + 'DLLs')

# 数据到来事件, 是以session ID为key的字典
dataEvents={}

# CTerm启动，每个实例触发一次
def OnCTermStart():
	print
	print time.asctime(datetime.datetime.now().timetuple())
	print 'CTerm started'
	#import os
	#os.startfile(r"some.exe")
	scriptdir=GetScriptDir()+"\\"
	execfile( scriptdir + "onstart.py" )
	#ConnectSite('202.118.1.82',23) # 启动时自动连接站点
	#ConnectSite('白云黄鹤★.null')
	#ConnectSite('水木清华★.null')

# CTerm关闭，每个实例触发一次
# 子窗口尚未关闭(若有)
def OnCTermClose():
	print
	print time.asctime(datetime.datetime.now().timetuple())
	print 'CTerm is closing'

# 连接成功
def OnSessionOpen(ID):
	print
	print time.asctime(datetime.datetime.now().timetuple())
	print "session %d connected\n"%ID
	# 初始化data事件
	dataEvents.setdefault(ID, threading.Event())

# 连接已断开
def OnSessionClose(ID):
	nowtime=datetime.datetime.now()
	print
	print time.asctime(nowtime.timetuple())
	print "session %d disconnected\n"%ID

	if dataEvents.has_key(ID):
		dataEvents.pop(ID)  # 删除事件
	#Reconnect(ID) #need enable not close window after disconnected option

# 防止发呆
def antiIdle(ID):
	idlestr=AntiIdleString()
	SendParsedString(ID,idlestr)
	#AntiIdleString('just test') //set
	#print "antiIdle"
	
# 数据到来并已处理显示完毕，状态识别也已完成
def OnDataCome(ID):
	#print "session %ddata comes"%ID
	#print
	#print GetText(ID,0,23)
	#print
	try:
		dataEvents[ID].set()
		dataEvents[ID].clear()
	except KeyError:
		pass
		#print 'KeyError'
		#print dataEvents, ID

def OnTimer(ID):
	#print "session %d OnTimer"%ID
	return

def OnLButtonUp(ID, x, y):
	#print "session %d OnLButtonUp"%ID
	return
	
def OnRButtonUp(ID, x, y):
	#print "session %d OnRButtonUp"%ID
	return
# 中键或鼠标滚轮按下
def OnMButtonUp(ID, x, y):
	#print "session %d OnMButtonUp"%ID
	SendString(ID,'\n')
	return

def OnKeyDown(ID, nChar, nRepCnt, nFlags):
	#print "session %d OnKeyDown"%ID
	return (0,)

#################################################################################
# 以下为URL识别部分

# 判断是否IP，返回正确小节数
# 1 是IP：n=count=4
# 2 疑问：n=count=3 需看后面是否.* 若是，则是IP，若否则连URL也不是
# 3 非IP亦非URL：全是数字的，却不是URL
# 0 非IP是URL：其它
def IsIP(str1):
	#print 'in IsIP %s'%str1
	nums=str1.split('.')
	n=len(nums)
	#if n<3 or n>4:
	#	return 0
	count=0
	bAllNumber=True
	for num in nums:
		try:
			num=int(num)
			if num<256 and num>=0:
				count+=1
		except ValueError:
			bAllNumber=False
			break
	if count==n:
		if n==4:
			return 1
		elif n==3:
			return 2
		else:  # bAllNumber must is True
			return 3
	elif bAllNumber:
		return 3
	else:
#		print str1, n, count
		return 0

# URL匹配正则表达式
epathch=r'[-\w\.\?\#=&\+~%\*@]'
pathchar=r'[-\w\.\?\#=&\+~%\*double]'
alnum=r'[a-zA-Z\d]'
double=r'\x80-\xff'

emailexp=r'(mailto:)?'\
'(alnum[-\w\.\+]{0,19})@'\
'((alnum[-\w]{0,9}\.){1,4}(alnum\w{0,9}))(:(0|[1-9][\d]{0,5}))?'
emailexp=emailexp.replace('alnum',alnum)

proxyexp=r'(([1-9]\d{0,2}\.){3}([1-9]\d{0,2}))(:(0|[1-9][\d]{0,5}))?'\
'@(http|socks[45]|telnet)'

urlexp1=r'([a-zA-Z]{3,8}://)?'\
'((alnum[-\w\.@]{0,29})(:(.{1,20}))?@)?'\
'((alnum[-\w]{0,19}\.){1,5}(alnum\w{0,9}))(:(0|[1-9][\d]{0,5}))?'\
'((/pathchar+|pathchar+\=http:/)*(/pathchar*[double]pathchar*\.(alnum{1,5})(?:[-\.\?\#\=&\+~%]epathch*)?|/epathch+(\.(alnum{1,5}))(?=[^-\w\.\?\#\=&\+~%@])|/epathch+|/))?'

# 特殊URL格式
calltoexp=r'callto://\w+'
# 注意: otherurl中不可带多余括号；若加括号，需要在OnParseURL()中untuple处相应的增加变量
otherurl=calltoexp

urlexp='('+proxyexp+')|('+emailexp+')|('+urlexp1+')|('+otherurl+')'
urlexp=urlexp.replace('epathch',epathch)
urlexp=urlexp.replace('pathchar',pathchar)
urlexp=urlexp.replace('alnum',alnum)
urlexp=urlexp.replace('double',double)

urlexp=re.compile(urlexp,re.I)
nonurlchar=r'[^-\w/\.@:\?\#$=&\+~%\*\x80-\xff]' #URL分隔符=除此之外的所有字符，遗漏太多，参RFC2396
urldelim=r'(.*'+nonurlchar+r')*'
urldelim=re.compile(urldelim)
nonurlchar=re.compile(nonurlchar)

topdomain=['com', 'edu', 'gov', 'int', 'mil', 'net', 'org', 'cn', 'hk', 'tw', 'mo', 'fr', 'ru', 'uk', 'de', 'au', 'ca', 
'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'as', 'at', 'aw', 'az', 'ba', 'bb', 'bd', 
'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'cc', 'cf', 'cg', 
'ch', 'ci', 'ck', 'cl', 'cm', 'co', 'cq', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 
'eg', 'eh', 'es', 'et', 'ev', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gh', 'gi', 'gl', 'gm', 
'gn', 'gp', 'gr', 'gt', 'gu', 'gw', 'gy', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'in', 'io', 'iq', 'ir', 'is', 
'it', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 
'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'mg', 'mh', 'ml', 'mm', 'mn', 'mp', 'mq', 'mr', 'ms', 'mt', 
'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nt', 'nu', 'nz', 'om', 'pa', 
'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rw', 'sa', 'sb', 'sc', 'sd', 
'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 
'tj', 'tk', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tz', 'ua', 'ug', 'us', 'uy', 'va', 'vc', 've', 'vg', 'vn', 'vu', 
'wf', 'ws', 'ye', 'yu', 'za', 'zm', 'zr', 'zw', 'asia', 'mobi', 'name', 'biz', 'tv', 'info']

IGNORE_TRAIL_BLANK=3 #忽略的最多行末空格数

# 为处理折行URL, 取前后各2行
def GetMoreLine(ID, x, y):
	'''取5行，前后各多两行'''	
	termwidth=GetTermWidth(ID)
	termheight=GetTermHeight(ID)

	s0=GetText(ID, y, -1, 1)
	#print x, y
	#print s0
	matchobj=nonurlchar.match(s0[x])
	#print 'you point to: %d:%s'%(x,s0[x])
	if matchobj: #所指字符为nonurlchar，直接抛出例外使调用函数返回
		raise Exception
	ignoreblanks=[]
	s2=s0.rstrip()
	str0=s2
	nBlank=len(s0)-len(s2)
	if nBlank<=IGNORE_TRAIL_BLANK:
		str0+='Z'*nBlank
		ignoreblanks.append((0,nBlank))
		i=1
		while i<=2 and y+i<termheight:
			s1=GetText(ID, y+i, -1, 1)
			if s1[0]!=' ':
				s2=s1.rstrip()
				str0+=s2
				#print '%d: %s'%(i, s1)
				nBlank=len(s1)-len(s2)
				if nBlank<=IGNORE_TRAIL_BLANK:
					str0+='Z'*nBlank
					ignoreblanks.append((i,nBlank))
				else:
					break
			else:
				break
			i+=1
	#print str0
	#print ignoreblanks
	i=1
	s=s0
	while i<=2 and y-i>=0:
		if s[0]!=' ':
			s1=GetText(ID, y-i, -1, 1)
			s2=s1.rstrip()
			nBlank=len(s1)-len(s2)
			if nBlank<=IGNORE_TRAIL_BLANK:
				str0=s2+'Z'*nBlank+str0
				x+=termwidth
				ignoreblanks.append((-i,nBlank))
				s=s1
				#print '%d: %s'%(-i, s1)
			else:
				break
		else:
			break
		i+=1
	#print str0
	ignoreblanks.sort()
	#print ignoreblanks
	return (str0, x, ignoreblanks)

# [\x80-\xff]匹配中文
# 中文URL的规则是：中间路径可含有汉字，
# 但最后的文件部分(末尾不是/)若含汉字，则必须以一个.xxx作为文件扩展名，x可为字母数字，长度为1-5
def OnParseURL(ID, x, y):
	''' 解析URL'''
	#print '\n\nin OnParseURL session#%d, x:%d, y:%d'%(ID, x, y)
	if IsConnected(ID):
		termwidth=GetTermWidth(ID)
		try:
			str1,x,ignoreblanks=GetMoreLine(ID,x,y)
			#print str1,x,ignoreblanks
		except Exception, errinfo:
			#print 'Error:'+str(errinfo)
			return ()
		if(str1): # may be None
			# 截头截尾，缩小范围
			matchobj=nonurlchar.search(str1, x)
			endpos=len(str1)
			if matchobj:
				endpos=matchobj.start()
			matchobj=urldelim.search(str1, 0, x)
			startpos=0
			if matchobj:
				startpos=matchobj.end()    # end()指向匹配串的后一个字符
			#print 'startpos, endpos:%d,%d'%(startpos, endpos)

			i=startpos
			while True:
				try:
					matchobj=urlexp.search(str1, i, endpos)
				except RuntimeError:
					print 'RuntimeError occurs when matching'
					return ()
				if matchobj:
					(start, end)=matchobj.span()
					#print "line#%d: '%s'"%(y,str1)
					#print 'url range:%d-%d,x:%d'%(start, end, x)

					(proxy,proxyip,non,non,non,proxyport,proxyproto,\
					email,emailheader,emailuser,emailserver,emailcompany,emailorg,non,emailport,\
					nomalurl,proto,non,user,non,pw,domain,company,org,non,port,\
					path,non,tail,cext,non,eext,other)=matchobj.groups() #untuple

					dellen=0
					ext=''
					if cext:
						ext=cext
					elif eext:
						ext=eext
					elif nomalurl and tail:
						#针对跨行url，下一行以http://开头即另一个url时，多识别一块的情况进行处理
						dotidx=tail.find('.')
						if dotidx!=-1:
							extstr=tail[dotidx:]
							matchobj1=re.search( r'Z{1,3}http', extstr )
							if matchobj1:
								Zidx=matchobj1.start()
								ext=extstr[1:Zidx]
								dellen=len(extstr)-Zidx
								#print 'dellen=', dellen
								end=end-dellen

					if start<=x and x<end:
						#7+8+11+6=32
						#print matchobj.groups()
						#print proxy,proxyip,proxyport,proxyproto #
						#print email,emailuser,emailserver,emailcompany,emailorg,emailport,
						#print nomalurl,proto,user,pw,domain,company,org,port,path,tail,cext,eext

						matchstring=matchobj.group(0)
						#print matchstring

						if dellen>0:
							matchstring=matchstring[0:-dellen]
						
						# 去除行尾空格
						y=x/termwidth
						#print 'match string: \n'+matchstring
						#print x, y, start, end
						blankpos=[]
						#print ignoreblanks
						for i,nBlank in ignoreblanks:
							if nBlank>0:
								blankEnd=(y+i+1)*termwidth-start
								blankStart=blankEnd-nBlank
								#print blankStart, blankEnd
								if blankStart==0:
									start+=nBlank
									matchstring=matchstring[nBlank:]
								elif blankStart>0:
									blankpos.append((blankStart,blankEnd))
						newmatchstring=''
						x0=0
						#print blankpos
						for blankStart,blankEnd in blankpos:
							newmatchstring+=matchstring[x0:blankStart]
							x0=blankEnd
						newmatchstring+=matchstring[x0:]
						matchstring=newmatchstring
						#print matchstring

						bIP=0
						ip=''

						if nomalurl and domain and domain.count('.')==1:
							if not org in topdomain:
								#print str1[end]
								#210.43.]
								if str1[end]=='.':
									tstr = matchstring + '.0.0'
									if IsIP(tstr)==1:
										bIP=4
										ip=tstr
								if not bIP:
									return () #排除大多数文件名
						
						bAutoHeader=GetConfigInt("AutoURLHeader")

						bPicture=0
						type=0
						if bIP!=4:
							if proxy:
								bIP=IsIP(proxyip)
								if bIP!=1:
									return ()
								type=2
								ip=proxyip
							elif email:
								bIP=IsIP(emailserver)
								if bIP==1:
									ip=emailserver
								elif bIP!=0:
									return ()
								type=1
								if bAutoHeader and not emailheader:
									matchstring='mailto:'+matchstring
							elif nomalurl:
								bIP=IsIP(domain)
								if bIP==2:	# 202.118.1.*
									idx=matchobj.span(22)[1] #domain's end index
									if str1[idx]=='.' and str1[idx+1]=='*':
										domain+='.0'
										matchstring+='.0'
										end+=2
										bIP=1
									else:
										return () #非URL，导致回调分析函数返回false
								elif bIP==3:
									return ()

								if bIP:
									ip=domain
								else:							
									if matchstring.find('/')==-1:
										if not org in topdomain:
											#支持前面有英文符的IP，如a10.8.0.2
											#kdfalkdfalkdf211.99.222.0
											if matchstring[-1]=='*' or re.match('[0-9]', matchstring[-1]):
												matchobj=re.search('[0-9]', matchstring)
												if matchobj:
													numpos=matchobj.start()
													if IsIP( matchstring[numpos:] )==1:
														bIP=1
														matchstring = matchstring[numpos:]
														ip=matchstring
														(domain,company,org) = ('','','')
														start = start + numpos
											if not bIP:
												return () #排除多级文件名及编程语句

							if nomalurl and bAutoHeader and not proto:	# 无协议头，自动分析添加协议头
								if not port or port not in ('80', '21' ,'22', '23', '8080'):
									if domain:
										if domain[0:3]=='www':
											matchstring='http://'+matchstring
										elif domain[0:3]=='ftp':
											matchstring='ftp://'+matchstring
										elif domain[0:3]=='bbs':
											matchstring='telnet://'+matchstring
								elif port=='80' or port=='8080':
									matchstring='http://'+matchstring
								elif port=='21':
									matchstring='ftp://'+matchstring
								elif port=='23' or port=='22':
									matchstring='telnet://'+matchstring

							if re.match(r"(?i)gif|jpg|jpeg|jpe|bmp|png|ani|tif|tiff|ico|jfif", ext):
								bPicture=1
							elif re.match(r'http://.*/(.*\.(?i)(gif|jpg|jpeg|jpe|bmp|png|ani|tif|tiff|jfif|ico).*|.*\.php\?bid=\d+&id=\d+&ap=\d+)', matchstring):
								bPicture=1
							#elif re.match(r'(?i)http://bbs.whnet.edu.cn/upload/.*B=Picture', matchstring):
							elif re.match(r'(?i).*Picture.*',matchstring):
								bPicture=1

						offset0,offset1=start-x,end-x
						if bIP==4:
							bIP=1
						ret=(matchstring, offset0, offset1, type, bIP, ip, ext, bPicture)
						#print ret
						return ret
					i=end
				else:
					break
		return ()

########################################################################################
# 状态识别部分

# 当前状态: 同样是以session ID为key的字典，值为tuple: (status, substatus)
# 在OnGetStatus时设置，不过，由于消息事件与状态事件分开，该变量将不能反映消息状态
siteStatus={}

# 从sitestatus.py导入站点配置数据
from sitestatus import *

# 识别状态
def OnGetStatus(ID):
	#print '============================================================'
	(status, sub)=GetScreenStatus(ID)
	siteStatus.setdefault(ID, (status, sub))
	return (status, sub)

# 未对消息进行识别，返回None，以便于使用执行内部识别算法
# 若识别：返回发消息ID与消息串，ID最长20字节，串最长12行即12*80字节
def OnGetMessage(ID):
	return

# 识别屏幕状态：ST_NORMAL/ST_MAPLE/FirBird 
# SMTH、Lily、YTHT都视为FireBird
# 台湾的 PTT、KKCity等类型视为Maple
def GetScreenStatus(ID):
	termheight=GetTermHeight(ID)

	special=GetSpecialTypeStatus(ID)
	if not special:
		type=GetSiteType(ID)
		#print "type: ", type
		if type==ST_NORMAL:
			return (SST_UNKNOWN, SST_SUB_NONE)
		elif type==ST_MAPLE:
			if len( GetText(ID,1).strip() )==0 and len( GetText(ID,termheight-2).strip() )==0: #for kkcity
				(status,sub)=(SST_MENU, SST_SUB_NONE)
			else:
				(status,sub)=GetStatusByKeyWord(ID, MapleKeywords)
		else:
			(status,sub)=GetStatusByKeyWord(ID, FBKeywords)
			#print GetText(ID,0)
			#print (status, sub)
	else:
		(status,sub)=special
	return (status,sub)

from prettyseq import PrettySeq

# 目前主要根据关键字识别状态
# 未来可以考虑更复杂些的办法：
# 指定cursor位置
# 关键字所在的行可指定为cursor.y
# 可指定关键字所在列(用正则亦能表达)
# 行列可用范围
# 关键字用正则表达式
# 关键字之间可为与的关系(用正则亦能表达)
# 其它条件
def GetStatusByKeyWord(ID, KeywordsDict):
	#print PrettySeq(KeywordsDict)
	termheight=GetTermHeight(ID)
	for st in statuspriority:
		if KeywordsDict.has_key(st):
			subkeys=KeywordsDict[st]
			#print PrettySeq(subkeys)
			if type(subkeys)==list:
				for line, keywords in subkeys:
					#处理相对行数，状态数据字典里相对行数只保存负值
					if line < 0:
						line = termheight + line
					#print line, PrettySeq(keywords)
					str0=GetText(ID, line)
					if InString(keywords, str0):
						#print 'st:%d'%st
						return (st, SST_SUB_NONE)
			elif type(subkeys)==dict: # 子状态
				#print PrettySeq(subkeys)
				for sub in subkeys.keys():
					keywords=subkeys[sub]
					if type(keywords)==list:
						for line, words in keywords:
							if line < 0:
								line = termheight + line
							#print line, PrettySeq(keywords)
							str0=GetText(ID, line)
							#print str0, words
							if InString(words, str0):
								#print (st, sub)
								return (st, sub)
					else: #tuple
						line, words=keywords
						if line < 0:
							line = termheight + line
						str0=GetText(ID, line)
						if InString(words, str0):
							#print (st, sub)
							return (st, sub)
			else : #tuple
				line, keywords=subkeys
				if line < 0:
					line = termheight + line
				str0=GetText(ID, line)
				if InString(keywords, str0):
					#print 'st:%d'%st
					return (st, SST_SUB_NONE)
	return (SST_UNKNOWN, SST_SUB_NONE)

# 可在此定义上述两种类型之外的特殊站点状态
def GetSpecialTypeStatus(ID):
	if len(SpecialTypeKeyWords)==0:
		return
	else:
		(status,sub)=GetStatusByKeyWord(ID, SpecialTypeKeyWords)
	print 'in special', (status,sub)
	if (status,sub)==(SST_UNKNOWN, SST_SUB_NONE):
		return None
	else:
		return (status,sub)

def InString(keywords, s):
	#print 'in InString'
	#print s
	for word in keywords:
		#print PrettySeq(word)
		if type(word)==tuple: #用单元素tuple表示正则表达式
			word=word[0]
			if re.search(word,s):
				#print 'matched 1'
				return True
		elif s.find(word)!=-1:
			#print 'matched 2'
			return True
	#print 'not match'
	return False
