/* 
	HLDS XMPP Client
	by serfreeman1337	http://gf.hldm.org/
*/

/* Special Thanks To:
	http://webi.ru/ for detailed XMPP protocol explore
	Black_Rose for base64 include
	and ConnorMcLeod for help
*/

#define PLUGIN_NAME	"HLDS XMPP Client"
#define PLUGIN_VERSION  "beta"
#define PLUGIN_AUTHOR	"serfreeman1337"

#define DATALEN		1024

#define ID_PREFIX	"hlds"

#define RECHECK_ID	5514
#define FETCH_ID	5513
#define PARSER_ID	5512
#define XSTATUS_ID	5511

#include <amxmodx>
#include <amxmisc>
#include <sockets>
#include <base64>

new nasok,current_state,cur_jid[64],bool:connected
new data[DATALEN],datasize,send[512],map[32],ifail

new host[64]
new port
new maxfail = 20
new domain[64]
new user[64]
new password[64]
new res[64]

new Float:parser_time
new Float:recheck_time
new Float:fetch_time
new Float:xstatus_time

new xstatus_template[512]
new say_template[512]

enum _:CURSTAT{
	HELLO,
	GETSRV_INFO,
	SECURE_SWITCH,
	LOGIN,
	GET_JID,
	RUN_SESSION,
	ONLINE
}

enum _:FRMT{
	SAY,
	XSTATUS
}

new Trie:g_allowfrom,Array:g_chatsubscript

public plugin_init(){
	register_plugin(PLUGIN_NAME,PLUGIN_VERSION,PLUGIN_AUTHOR)
	
	g_allowfrom = TrieCreate()
	g_chatsubscript = ArrayCreate(64)
	
	register_clcmd("say","Say_Func")
	
	//set_task(5.0,"Init_Connection")
}

public plugin_cfg(){
	new path[512]
	
	get_configsdir(path,511)
	formatex(path,511,"%s/xmpp.ini",path)
	
	new f = fopen(path,"r")
	
	if(!f)
		return
		
	new s
		
	while(!feof(f)){
		new buff[512]
		fgets(f,buff,511)
		
		trim(buff)
		buff[strlen(buff)] = 0
		
		if(!strlen(buff))
			continue
			
		if(!strcmp(buff,"[xmpp]")){
			s = 1
			
			continue
		}
		
		if(!strcmp(buff,"[xmpp_refresh]")){
			s = 2
			
			continue
		}
		
		if(!strcmp(buff,"[xmpp_allow]")){
			s = 3
			
			continue
		}
		
		if(!strcmp(buff,"[xmpp_template]")){
			s = 4
			
			continue
		}
		
		switch(s){
			case 1:{
				new key[16],val[64]
			
				strtok(buff,key,15,val,63,'=',1)
				replace_all(val,63,"= ","")
				
				if(!strcmp(key,"host")){
					copy(host,strlen(val),val)
					
					continue
				}
				
				if(!strcmp(key,"port")){
					port = str_to_num(val)
					
					continue
				}
				
				if(!strcmp(key,"domain")){
					copy(domain,strlen(val),val)
					
					continue
				}
				
				if(!strcmp(key,"user")){
					copy(user,strlen(val),val)
					
					continue
				}
				
				if(!strcmp(key,"password")){
					copy(password,strlen(val),val)
					
					continue
				}
				
				if(!strcmp(key,"res")){
					copy(res,strlen(val),val)
					
					continue
				}
			}
			case 2:{
				new key[16],val[32]
				strtok(buff,key,15,val,31,'=',1)
				replace_all(val,31,"= ","")
				
				if(!strcmp(key,"recheck")){
					recheck_time = str_to_float(val)
						
					continue
				}
					
				if(!strcmp(key,"fetch")){
					fetch_time = str_to_float(val)
					
					continue
				}
				
				if(!strcmp(key,"parse")){
					parser_time = str_to_float(val)
					
					continue
				}
				
				if(!strcmp(key,"xstatus")){
					xstatus_time = str_to_float(val)
					
					continue
				}
				
				if(!strcmp(key,"maxfail")){
					maxfail = str_to_num(val)
					
					continue
				}
			}
			case 3:{
				TrieSetCell(g_allowfrom,buff,1)
			}
			case 4:{
				new key[16],val[512]
				strtok(buff,key,15,val,511,'=',1)
				replace_all(val,511,"= ","")
				remove_quotes(val)
				
				if(!strcmp(key,"xstatus")){
					copy(xstatus_template,511,val)
					
					continue
				}
				
				if(!strcmp(key,"say")){
					copy(say_template,511,val)

					continue
				}
			}
		}
		
	}
	
	fclose(f)
	
	get_configsdir(path,511)
	formatex(path,511,"%s/xmpp_subscript.ini",path)
	
	f = fopen(path,"r")
	
	if(f){
		while(!feof(f)){
			new buff[128]
			fgets(f,buff,127)
			
			trim(buff)
			buff[strlen(buff)] = 0
		
			if(!strlen(buff))
				continue
			
			ArrayPushString(g_chatsubscript,buff)
		}
		
		fclose(f)
	}
	
	set_task(3.0,"Init_Connection")
}

public plugin_end(){
	remove_task(FETCH_ID)
	remove_task(PARSER_ID)
	remove_task(RECHECK_ID)
	remove_task(XSTATUS_ID)
	
	socket_close(nasok)
}

public Get_Data(){
	data[0] = 0
	datasize = 0
	
	set_task(fetch_time,"Recieve_Data",FETCH_ID)
}

public Init_Connection(){
	new err
	
	remove_task(FETCH_ID)
	remove_task(PARSER_ID)
	remove_task(RECHECK_ID)
	remove_task(XSTATUS_ID)
	
	socket_close(nasok)
	
	nasok = socket_open(host,port,SOCKET_TCP,err)
	
	if(err){
		server_print("[XMPP] Couldnt connect to %s",host)
	}else{
		server_print("[XMPP] Connected to %s",host)
		set_task(0.1,"Init_Hello")
	}
}

public Init_Hello(){
	current_state = HELLO
	Send_Hello()		// DARAVA!
}

public Send_Hello(){
	formatex(send,511,"<?xml version=^"1.0^"?>\
		<stream:stream xmlns:stream=^"http://etherx.jabber.org/streams^" version=^"1.0^" xmlns=^"jabber:client^" to=^"%s^" xml:lang=^"en^" xmlns:xml=^"http://www.w3.org/XML/1998/namespace^">",
		domain)
	socket_send(nasok,send,strlen(send))
	
	set_task(fetch_time,"Recieve_Data",FETCH_ID)
}

public Attempt_Login(){
	new auth[512], auth2[512]
	formatex(auth,511,"%c%s%c%s",0x00,user,0x00,password)
    
	Encode64(auth, ( strlen(user) + strlen(password) ) + 2, auth2, 511)
 
	formatex(send,511,"<auth xmlns=^"urn:ietf:params:xml:ns:xmpp-sasl^" mechanism=^"PLAIN^">%s</auth>",auth2)
	
	socket_send(nasok,send,strlen(send))
	
	set_task(fetch_time,"Recieve_Data",FETCH_ID)
}

public Get_JID(){
	formatex(send,511,"<iq type=^"set^" id=^"%s_1^"><bind xmlns=^"urn:ietf:params:xml:ns:xmpp-bind^"><resource>%s</resource></bind></iq>",
		ID_PREFIX,res)
	socket_send(nasok,send,strlen(send))
	
	set_task(fetch_time,"Recieve_Data",FETCH_ID)
}

public Launch_Session(){
	formatex(send,511,"<iq type=^"set^" id=^"%s_2^" to=^"%s^"><session xmlns=^"urn:ietf:params:xml:ns:xmpp-session^"/></iq>",ID_PREFIX,domain)
	socket_send(nasok,send,strlen(send))

	set_task(fetch_time,"Recieve_Data",FETCH_ID)
}

public Become_Online(){
	get_mapname(map,31)
	
	connected = true
	
	set_task(0.5,"Send_XStatus")
	
	set_task(recheck_time,"Recieve_Data",RECHECK_ID)
}

public Send_XStatus(){
	new xstatus[256]
	Parse_Template(1,xstatus,255,0)
	
	formatex(send,511,"<presence><show></show><status>%s</status><priority>10</priority></presence>",xstatus)
	socket_send(nasok,send,strlen(send))
	
	set_task(xstatus_time,"Send_XStatus",XSTATUS_ID)
}

public Parse_Template(tpl,string[],lev,id){
	new templ[512],temp[32]
	
	if(tpl==1)
		copy(templ,511,xstatus_template)
	else if(tpl==2)
		copy(templ,511,say_template)
	
	if(contain(templ,"%hostname%")!=-1){
		get_cvar_string("hostname",temp,31)
		replace_all(templ,511,"%hostname%",temp)
	}
	
	if(contain(templ,"%plrs%")!=-1){
		num_to_str(get_playersnum(),temp,31)
		replace_all(templ,511,"%plrs%",temp)
	}
	
	if(contain(templ,"%maxplrs%")!=-1){
		num_to_str(get_maxplayers(),temp,31)
		replace_all(templ,511,"%maxplrs%",temp)
	}
	
	if(contain(templ,"%mapname%")!=-1){
		get_mapname(temp,31)
		replace_all(templ,511,"%mapname%",temp)
	}
	
	if(contain(templ,"%timeleft%")!=-1){
		get_cvar_string("amx_timeleft",temp,31)
		replace_all(templ,511,"%timeleft%",temp)
	}
	
	if(contain(templ,"%username%")!=-1){
		get_user_name(id,temp,31)
		replace_all(templ,511,"%username%",temp)
	}
	
	if(contain(templ,"%steamid%")!=-1){
		get_user_authid(id,temp,31)
		replace_all(templ,255,"%steamid%",temp)
	}
	
	if(contain(templ,"%ip%")!=-1){
		get_user_ip(id,temp,31,1)
		replace_all(templ,511,"%ip%",temp)
	}
	
	if(contain(templ,"%frags%")!=-1){
		num_to_str(get_user_frags(id),temp,31)
		replace_all(templ,511,"%frags%",temp)
	}
	
	if(contain(templ,"%deaths%")!=-1){
		num_to_str(get_user_deaths(id),temp,31)
		replace_all(templ,511,"%deaths%",temp)
	}
	
	if(contain(templ,"%ping%")!=-1){
		new ping,loss
		get_user_ping(id,ping,loss)
		
		num_to_str(ping,temp,31)
		replace_all(templ,511,"%ping%",temp)
	}
	
	copy(string,lev,templ)
}

public Recieve_Data(dd){
	new buff[512]
	
	if(socket_change(nasok,1000))
		socket_recv(nasok,buff,511)
		
	replace_all(buff,511,"%","")
	replace_all(buff,511,"&quot;","^"")
	
	if(DATALEN<=datasize){
		//log_amx("too big entry!")
		
		// wtf
		
		Proccess_Data()
		
		return
	}
	
	datasize += formatex(data[datasize],DATALEN-datasize,buff)
	
	if(!strlen(buff)){
		if(strlen(data)&&data[datasize-1]=='>'){
			Proccess_Data()
			
			data[0] = 0
			datasize = 0
		}

		if(current_state!=ONLINE){
			return
		}else{
			set_task(recheck_time,"Recieve_Data",RECHECK_ID)
			
			return
		}
	}else{
		set_task(parser_time,"Recieve_Data",PARSER_ID)
	}
}
public Proccess_Data(){
	new num,temp[32]
	
	if(ifail>=maxfail){
		log_amx("maxfail is reached")
		
		socket_close(nasok)
		
		return
	}
	
	switch(current_state){
		case HELLO:{
			num = strfind(data,"id",0,num)
			
			if(!num){
				log_amx("unable to get ID")
				socket_close(nasok)
				
				return
			}
			
			num = strfind(data,"'",0,num)
			
			formatex(temp,strfind(data,"'",0,num+1)-num-1,"%s",data[num+1])
			
			num = strfind(data,"<mechanism>",0,num)
			
			if(num==-1){
				ifail ++
				
				set_task(0.3,"Recieve_Data")
				
				return
			}
			
			get_tag_contaiment(data,"<mechanism>","</mechanism>",temp,31)

			if(strcmp(temp,"PLAIN")==-1){
				server_print("[XMPP] UnKnown auth method ^"%s^"",temp)
				log_amx("unknown auth method ^"%s^"",temp)
				
				socket_close(nasok)
				
				return
			}
			
			server_print("[XMPP] Logging in...")
			
			current_state = LOGIN
			Attempt_Login()
		}
		case LOGIN:{
			if(datasize<=100){
				if(contain(data,"success")!=-1){
					Send_Hello()
				}else{
					if(contain(data,"bad-auth")){
						server_print("[XMPP] Incorrect Login")
						log_amx("incorrect login")
					}
					else{
						server_print("[XMPP] Unable to login")
						log_amx("unable to login")
					}
					
					socket_close(nasok)
					
					return
				}
				
				return
			}
			
			current_state = GET_JID
			Get_JID()
		}
		case GET_JID:{
			get_tag_contaiment(data,"<jid>","</jid>",cur_jid,63)
			
			if(!strlen(cur_jid)){
				server_print("[XMPP] Failed to get JID")
				server_print("[XMPP] Retrying...")
				
				log_amx("failed to retrieve JID")
				
				ifail ++
				
				set_task(3.0,"Init_Connection")
				
				return
			}
			
			current_state = RUN_SESSION
			Launch_Session()
		}
		case RUN_SESSION:{
			if(contain(data,"xmpp-session")!=-1){
				current_state = ONLINE
				Become_Online()
				
				server_print("[XMPP] Connected as %s",cur_jid)
				
				ifail = 0
			}else{
				
				return
			}
		}
		case ONLINE:{
			add(data,DATALEN,"^n")
			
			new type[16],id[16],to[64],from[64]
			
			get_tag_contaiment(data,"to='","'",to,63)
			get_tag_contaiment(data,"from='","'",from,63)
			get_tag_contaiment(data,"type='","'",type,15)
			get_tag_contaiment(data,"id='","'",id,15)
			
			if(strcmp(to,cur_jid)==-1)
				return
				
			new otparsil[64]
			copy(otparsil,63,from)
			
			new grv = strfind(otparsil,"/")
			
			if(grv!=-1)
				otparsil[grv] = 0
				
			if(!TrieKeyExists(g_allowfrom,otparsil))
				return
			
			switch(type[0]){
				case 'g':{
					if(contain(data,"jabber:iq:version")!=-1){
						Send_Client_Version(from,id)
					}else if(contain(data,"jabber:iq:last")){
						Send_Last_Time(from,id)
					}
				}
				case 's':{
					if(strcmp(type,"subscribe")==-1)
						return
						
					formatex(send,511,"<presence to=^"%s^" type=^"subscribed^" />",from)
					socket_send(nasok,send,strlen(send))
					
					formatex(send,511,"<presence to=^"%s^" type=^"subscribe^" />",from)
					socket_send(nasok,send,strlen(send))
				}
				case 'c':{
					new chatmsg[512]
					get_tag_contaiment(data,"<body>","</body>",chatmsg,511)
					
					Recieve_Message(from,chatmsg)
				}
			}
		}
	}
}

public Send_Client_Version(to[],id[]){
	formatex(send,511,"<iq type=^"result^" to=^"%s^" id=^"%s^">\
				<query xmlns=^"jabber:iq:version^">\
					<name>%s</name>\
					<version>%s</version>\
					<os>%s</os>\
				</query>\
			</iq>",to,id,PLUGIN_NAME,PLUGIN_VERSION,"HLDS Server")

	socket_send(nasok,send,strlen(send))
}

public Send_Last_Time(to[],id[]){
	formatex(send,511,"<iq type=^"result^" to=^"%s^" id=^"%s^">\
				<query xmlns=^"jabber:iq:last^" seconds=^"0^" />\
			</iq>",to,id)
			
	socket_send(nasok,send,strlen(send))
}

public Recieve_Message(from[],msg[]){
	new temp[128]
	
	if(contain(msg,"chat")==0){
		replace(msg,10,"chat","")
		replace(msg,10," ","")
		
		if(!strcmp(msg,"on")){
			if(Check_Chat_SubScript(from)){
				Send_Message(from,"Already subscripted")
			}else{
				ArrayPushString(g_chatsubscript,from)
				Write_Subscript_File()
				
				Send_Message(from,"Successfully subscripted to server chat messages")
			}
		}else if(!strcmp(msg,"off")){
			if(!Check_Chat_SubScript(from)){
				Send_Message(from,"Not subscripted yet")
			}else{
				if(Delete_Chat_SubScript(from))
					Send_Message(from,"Successfully unsubscripted from server chat messages")
				
				Write_Subscript_File()
			}
		}else if(!strcmp(msg,"status")){
			if(Check_Chat_SubScript(from)){
				Send_Message(from,"Server chat subscription status: Subscripted")
			}else{
				Send_Message(from,"Server chat subscription status: Unsubscripted")
			}
		}else{
			formatex(temp,127,"Unknown command ^"%s^" ^nAvaliable commands for chat subscript are: on | off | status",msg)
			Send_Message(from,temp)
		}
		
		return
	}
	
	if(contain(msg,"rcon")==0){
		replace(msg,strlen(msg),"rcon","")
		replace(msg,strlen(msg)," ","")
		
		if(!strlen(msg))
			return
		
		formatex(temp,127,"Command ^"%s^" sended to server console.",msg)
		Send_Message(from,temp)
		
		server_cmd(msg)
		server_exec()
		
		return
	}
	
	if(contain(msg,"cvar")==0){
		replace(msg,strlen(msg),"cvar","")
		replace(msg,strlen(msg)," ","")
		
		if(!strlen(msg))
			return
			
		new string[64]
		get_cvar_string(msg,string,63)
		
		formatex(temp,127,"  '%s' = '%s'",msg,string)
		Send_Message(from,temp)
		
		return
	}
	
	if(contain(msg,"status")==0){
		new messag[512],len
		
		len += formatex(messag[len],511-len," #  | #ID	Nick	  SteamID	IP	Frags/Deaths	Ping/Loss^n^n")
		
		new players[32],pnum
		get_players(players,pnum)
		
		for(new i;i<pnum;++i){
			new name[32],auth[32],ip[32],ping,loss
			get_user_name(players[i],name,31)
			get_user_authid(players[i],auth,31)
			get_user_ip(players[i],ip,31,1)
			get_user_ping(players[i],ping,loss)
			
			len += 	formatex(messag[len],511-len," %d | #%d	%s	%s	%s	%d/%d	%d/%d^n",
				i+1,get_user_index(name),name,auth,ip,get_user_frags(players[i]),get_user_deaths(players[i]),ping,loss)
		}
		
		//en += formatex(messag[len],511-len,"----^nCurrent Players: %s^nCurrent Map: %s",pnum,map)
		
		Send_Message(from,messag)
		
		return
	}
	
	new grv = strfind(from,"/")
			
	if(grv!=-1)
		from[grv] = 0
		
	client_print(0,print_chat," %s: %s",from,msg)
}

public Say_Func(id){
	if(!connected||!ArraySize(g_chatsubscript))
		return
	
	new args[128],name[32],msg[512]
	read_args(args,127)
	get_user_name(id,name,31)
	remove_quotes(args)
	
	if(!strlen(args))
		return
	
	Parse_Template(2,msg,511,id)
	replace_all(msg,511,"%message%",args)

	for(new i;i<ArraySize(g_chatsubscript);++i){
		new jid[64]
		ArrayGetString(g_chatsubscript,i,jid,63)
		
		Send_Message(jid,msg)
	}
}

public Send_Message(to[],msg[]){
	new dsend[1024]
	
	formatex(dsend,1023,"<message type=^"chat^" from=^"%s^" to=^"%s^" id=^"%s_%d^"><body>%s</body></message>",
		cur_jid,to,ID_PREFIX,random_num(0,1337),msg)
	socket_send(nasok,dsend,1024)
	
}

public Write_Subscript_File(){
	new path[512]
	
	get_configsdir(path,511)
	formatex(path,511,"%s/xmpp_subscript.ini",path)
	
	new f = fopen(path,"w+")
	
	for(new i;i<ArraySize(g_chatsubscript);++i){
		new jid[64]
		ArrayGetString(g_chatsubscript,i,jid,63)
		add(jid,63,"^n")
		fputs(f,jid)
	}
	
	fclose(f)
}

public Check_Chat_SubScript(jid[]){
	for(new i;i<ArraySize(g_chatsubscript);++i){
		new temp[64]
		ArrayGetString(g_chatsubscript,i,temp,63)
		
		if(!strcmp(temp,jid))
			return true
	}
	
	return false
}

public Delete_Chat_SubScript(jid[]){
	for(new i;i<ArraySize(g_chatsubscript);++i){
		new temp[64]
		ArrayGetString(g_chatsubscript,i,temp,63)
		
		if(!strcmp(temp,jid)){
			ArrayDeleteItem(g_chatsubscript,i)
			
			return true
		}
	}
	
	return false
}

stock get_tag_contaiment(string[],opentag[],endtag[],out[],outlen){
	new pos
	
	pos = strfind(string,opentag,0,pos)
	
	if(pos==-1)
		return 0
	
	pos += strlen(opentag)
	
	new endpos = strfind(string,endtag,0,pos)
	
	if(endpos==-1)
		return 0
		
	new dtlen = endpos - pos
	formatex(out,dtlen<=outlen?dtlen:outlen,"%s",string[pos])
	
	return 1
}
