#!/usr/bin/env python
'''Zulk-BOT

VERSION: 1.0.0
NOTE: this is probably the last release before version 1.0

Made in sweden by TheZulk ( thezulk at yahoo.com )

Feel free to use this code as you like. Just give me some credit :)

The code is quick and ugly... Don't expect to be able to understand it!
(I've added some comments to make it somewhat readable)

All code is written with nedit under gentoo linux with XFree86 4.2.0 running xfce
Many thanks to xchat and it's "RAW log window" that provided me with the information i needed
and ofcourse the developers behind python for this superb scripting language

I realy hate the rfc:s and hope someone creates some sort of tool for making it easy to read them ;)

Please send me bugreports!

Known bugs:
	1) some things won't be deleted with the "del" command (i won't fix it unless someone ask me to)
	2) i'm tierd and hungry

Day one:   I've had my eves on the IRC protocol for a while now.
			Using parts from old socket-based programs the bot was up and running in notime.
			Don't know how ctcp and dcc requests work yet... looks like a special character
			at the beginning and the end of a PRIVMSG. Bot is programmable over PRIVMSG and
			it responds to the right source.

Day two:   It's now possible to make the bot respond with RAW IRC commands aswell as the usual
			source looked up PRIVMSG. Commands can be made only to respond to users thar are
			loged in. CTCP is now working. Hardcoded VERSION and PING replies

Day three: Now you can add searchpatterns for other things than PRIVMSG. Added some tests to
			look for errors. The bot will now try to reconnect when disconnected. "§" now
			translates to the binary value 1, thus making it possible to send CTCP requests.

Day four:  This is now the forth day I'm working on this project, and it's getting better (I hope).
			I've added some "try, except" statements so that errors in "err_hand" shouldn't
			bring the bot down. Fixed the reconnect feature so that it will try to reconnect more
			than once. Added comments and this work log.


Usage:
	tnet.py [irc.server.net]
	
Commands:
	All commands are to be written to the bot in a channel or through "/msg" or "/query"
	Start the command with the current nick of the bot
	Search patterns are python regular expressions (type "pydoc sre" for more help)
	
		add - Adds a PRIVMSG searchpattern and a response to the pattern (will say the response to the channel or person it found it from)
		raw - Adds a PRIVMSG searchpattern and a response to the pattern (the response is a RAW IRC command)
		del - Deletes a searchpattern 
		
		radd - Adds a raw searchpattern and a response to the pattern (will say the response to the channel or person it found it from)
		rraw - Adds a raw searchpattern and a response to the pattern (the response is a RAW IRC command)

		login - Log in using password
		passwd - change password on the fly
		
		do - Sends RAW IRC command
		quit - Disconnects from IRC


Translated codes:
	*NICK*	- translates to the nick of the person that sent the message that is being replied to
	§		- translates to the binary value 1 (used for sending CTCP requests)

Examples:

	> /msg bot bot login passwd
	<me> bot login passwd
	<bot> Loged in

	> bot add hello.*bot
	<me> bot add hello.*bot hi *NICK*
	<bot> Added "hello.*bot" to list

	> hello there bot
	<me> hello there bot
	<bot> hi me

	> bot do PRIVMSG me :§VERSION§
	-Received a CTCP VERSION from bot

'''

from sys import argv
from os import environ
from socket import *
from threading import _start_new_thread
from time import sleep
from os.path import exists
import re
import pickle

global sock,curNick,iName,fString,passwd,running,server,configFile

progVer = '1.0.0'

######################## Settings ############################

# A list with nicks to select from
orgNickList = ['zulk','zulkis','mupp_zlk','zulk_bot']

# the user to be looked for with auth on connect
iName = 'thezulk'

# the identification string
fString = 'I\'m a zulk-bot'

# the default password
passwd = 'stdPass'

# version reply
version = 'Zulk-bot version ' + progVer + '. Made from scratch in python by TheZulk'

# the standard server
server = 'irc.du.se'

# commands that will be sent when succesfully connected
onConnect = ['JOIN #ldream','JOIN #beepdealers']

# dict with lists containing commands to be sent when joining the corresponding channel
onJoin = {'#ldream':['PRIVMSG ^NucDawN^ :op getter3g','PRIVMSG #ldream :Hi all! :)']}

##################### Leave these blank #######################

# the message stack used in "getString()"
strStack = []

# temporary string used in "getString()"
string = ''

# dict with search patterns and replies
comList = {}

# list containing autherized users
authUsers = []

# the configuration file
configFile = ''

##################### Code stars here #########################
nickList = orgNickList+[]
curNick = nickList.pop(0)

def getString():
	'''Stripps down data from IRC server and returns the next server command'''
	global sock,strStack,string,server
#	is the message stack empty?
	if len(strStack) <= 0:
#		catch errors
		try:
#			get messages from server
			text = sock.recv(1024)
#		has an error acured?
		except:
#			catch errors
			try:
#				close the connection
				sock.close()
			except:
				pass
#			reconnect to the server
			return ''
#		append the server messages to the message stack
		for a in text:
			if a == '\r':
				strStack.append(string)
				string = ''
				continue
			if a == '\n':
				continue
			string += a
#	is the stack still empty?
	if len(strStack) == 0:
#		return an empty string
		return ''
#	return the next command from the stack
	return strStack.pop()

def err_hand(inText):
	'''Handles "all" server commands'''
	global comList,passwd,authUsers,curNick,nickList,version,configFile,running

#	define "text" as a string
	text = ''

#	Look if it's a command
	if inText[0] == ':':

#		Create usefull lists
#		a typical RAW IRC command from the server:
#		:TheZulk!~thezulk@my-host.se PRIVMSG #mychannel :hello
#		This will get split up to ['','TheZulk!~thezulk@my-host.se PRIVMSG #mychannel ','hello']
		l1 = inText.split(':',2)

#		More splitting:
#		It will now be splitted to: ['TheZulk!~thezulk@my-host.se','PRIVMSG','#mychannel']
		l2 = l1[1].split()

#		Test what command it is

#		is it 376? That means "End of motd" and is sent on a succesfull connact
		if l2[1] == '376':
#			Send the commands defined in the onConnect list
			for a in onConnect:
				sock.send(a+'\r\n')
				print a
#		is it 433? That means "Nick already taken" and the nick have to be changed
		if l2[1] == '433':
#			Are there any nicks left in nickList?
			if len(nickList) == 0:
#				If not, fill it upp with the original list
				nickList = orgNickList+[]
#			Get the next nick from nickList and erase it from the list
			curNick = nickList.pop(0)
#			Set the nick
			return('NICK '+curNick+'\r\n')

#		is someone joining?
		elif l2[1] == 'JOIN':
#			is it me?
			if l2[0].split('!')[0] == curNick:
#				do we want to do stuff when joining this channel?
				if onJoin.has_key(l1[2]):
#					do the stuff in onJoin[channel]
					for a in onJoin[l1[2]]:
						sock.send(a+'\r\n')
						print '<me> ' + a

#		is someone changing their nick?
		elif l2[1] == 'NICK':
			oldNick = l2[0].split('!')[0]
#			is it me?
			if oldNick == curNick:
#				remember chat nick i'm using now
				curNick = l1[2]
#			is it someone that is loged in?
			for a in range(len(authUsers)-1):
				if oldNick == authUsers[a]:
					authUsers[a] = l1[2]

#		did someone say something?
		elif l2[1] == 'PRIVMSG':
			#print str(ord(l1[2][0])) + ' <--> ' + str(ord(l1[2][-1]))

#			Split the text said to [command,first_word,rest]
			l3 = l1[2].split(' ',3)

#			is it ctcp?
			if ord(l1[2][0]) == 1:
				text = 'NOTICE ' + l2[0].split('!')[0] + ' :'
				tmp = l1[2][1:-1]
#				did someone request a version check?
				if tmp == 'VERSION':
					return text + chr(1) + 'VERSION ' + version + chr(1)
#				did someone request a ping?
				if tmp[:4] == 'PING':
					return text + chr(1) + 'PING' + tmp[4:] + chr(1)

#			Where should we respond to?

#			Where the message sent directly to me?
			if l2[2] == curNick:
#				respond directly to the sender
				text = 'PRIVMSG ' + l2[0].split('!')[0] + ' :'
			else:
#				in other case, respond to the channel
				text = 'PRIVMSG ' + l2[2] + ' :'

#			ís the first word my current nick?
			if l3[0] == curNick:
#				are there a command supplied after the nick?
				if len(l3) > 1:

#					is it the "add" commend?
					if l3[1] == 'add':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							l3[2] = re.sub('§',chr(1),l3[2])
#							check if message has enough words
							if len(l3) > 2:
#								add the searchpattern to comList
								comList['^:.*PRIVMSG.*:'+l3[2]] = 'P' + l3[3]
#								save comList to config file
								pickle.dump(comList,file(configFile,'w'))
#								respond with what has been done
								return text + 'Added "' + l3[2] + '" to list'
							return text + 'error'
						else:
							return text + 'Please log in first'

#					is it the "radd" commend?
					if l3[1] == 'radd':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							l3[2] = re.sub('§',chr(1),l3[2])
#							check if message has enough words
							if len(l3) > 2:
#								add the searchpattern to comList
								comList[l3[2]] = 'P' + l3[3]
#								save comList to config file
								pickle.dump(comList,file(configFile,'w'))
#								respond with what has been done
								return text + 'Added "' + l3[2] + '" to list'
							return text + 'error'
						else:
							return text + 'Please log in first'

#					is it the "del" commend?
					elif l3[1] == 'del':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							l3[2] = re.sub('§',chr(1),l3[2])
#							check if the pattern excists in comList
							if comList.has_key(l3[2]):
#								delete the search pattern
								del comList[l3[2]]
#								respond with what has been done
								text += 'Deleted "' + l3[2] + '"'
#								save comList to config file
								pickle.dump(comList,file(configFile,'w'))
							else:
#								respond with an error message
								return text + 'Not found'
						else:
							return text + 'Please log in first'

#					is it the "dir" commend?
					elif l3[1] == 'dir':
						return text + str(comList.keys())

#					is it the "raw" commend?
					elif l3[1] == 'raw':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							l3[2] = re.sub('§',chr(1),l3[2])
#							check if message has enough words
							if len(l3) > 2:
#								add the searchpattern to comList
								comList['^:.*PRIVMSG.*:'+l3[2]] = 'R'+l3[3]
#								save comList to config file
								pickle.dump(comList,file(configFile,'w'))
#								respond with what has been done
								return text + 'Added raw "' + l3[2] + '" to list'
							return text + 'error'
						else:
							return text + 'Please log in first'

#					is it the "rraw" commend?
					elif l3[1] == 'rraw':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							l3[2] = re.sub('§',chr(1),l3[2])
#							check if message has enough words
							if len(l3) > 2:
#								add the searchpattern to comList
								comList[l3[2]] = 'R'+l3[3]
#								save comList to config file
								pickle.dump(comList,file(configFile,'w'))
#								respond with what has been done
								return text + 'Added raw "' + l3[2] + '" to list'
							return text + 'error'
						else:
							return text + 'Please log in first'

#					is it the "login" commend?
					elif l3[1] == 'login':
#						was the right password specified?
						if l3[2] == passwd:
#							add the user to "authUsers"
							authUsers.append(l2[0])
#							respond with what has been done
							return text + 'Loged in'
						else:
							return text + 'Wrong password'

#					is it the "passwd" commend?
					elif l3[1] == 'passwd':
#						check if user is logged in
						if l2[0] in authUsers:
							passwd = l3[2]
#							respond with what has been done
							return text + 'New password set to "' + passwd + '"'
						else:
							return text + 'Please log in first'

#					is it the "do" commend?
					elif l3[1] == 'do':
#						check if user is logged in
						if l2[0] in authUsers:
#							replace all '§':s to the binary value of one (for ctcp)
							text = re.sub('§', chr(1) ,l3[2]+' '+l3[3])
#							send the RAW IRC command
							return text
						else:
							return text + 'Please log in first'

#                                       is it the "read-conf" commend?
                                        elif l3[1] == 'read-conf':
#                                               check if user is logged in
                                                if l2[0] in authUsers:
						        if exists(configFile):
#								load the configuration
						                comList = pickle.load(file(configFile,'r'))
                                                        	return text + 'Reread!'
                                                        return text + 'No config file!'
                                                else:
                                                        return text + 'Please log in first'

#					is it the "quit" commend?
					elif l3[1] == 'quit':
#						check if user is logged in
						if l2[0] in authUsers:
#							set "running" to false so the bot doesn't try to reconnect
							running = 0
#							send the RAW IRC command
							return 'QUIT ' + l3[2]+' '+l3[3]
						else:
							return text + 'Please log in first'
					else:
#						send standard answer
						return text + 'Va? Vad säger du?'
				else:
#					send standard answer
					return text + 'ja? vad vill du?'
			tmp = text.split(':')
			if len(tmp) > 1:
				if tmp[1] == '':
					text = ''
#	did the server PING me?
	elif inText[:4] == 'PING':
#		send PONG
		return('PONG :' + inText.split(':')[1])

	#print 'Testing: ' + inText
#	this shouldn't need to bee here, but tests if text is longer than nothing
	if len(text):
#		send the RAW IRC command in "text"
		return text

#	split the server command (described before)
	l1 = inText.split(':',2)
	l2 = l1[1].split()
#	check if we should trace the origin
	if len(l2) > 2:
		if l2[2] == curNick:
			text = 'PRIVMSG ' + l2[0].split('!')[0] + ' :'
		else:
			text = 'PRIVMSG ' + l2[2] + ' :'

#	look in comList if there are any patterns that match the one we got
	for a in comList.keys():
#		did this one match?
		if re.search(a,inText):
#			was this added with the "add" or "radd" command?
			if comList[a][0] == 'P':
				text += comList[a][1:]
#			was this added with the "raw" or "rraw" command?
			if comList[a][0] == 'R':
				text = comList[a][1:]
#			is it password protected?
			if text[0] == '!':
				text = text[1:]
				if not l2[0] in authUsers:
					text = ''
					continue
#			replace "*NICK*" with the nick of the sender
			text = re.sub('\*NICK\*', l2[0].split('!')[0] ,text)
#			replace all '§':s to the binary value of one (for ctcp)
			text = re.sub('§', chr(1) ,text)
			return text

	return ''



def read_sock():
	global sock,running,server
	print 'Thread started'

#	this is the main message loop
	while running:
#		get server message
		text = getString()
#		did we get an empty string?
		if text == '':
#			should we try to reconnect?
			if running:
#				try to reconnect
				sock.close()
				connect(server,6667)
				continue
#		print what the server said
		print '<server> ' + text

#		catch errors
		try:
#			handle the server message
			outText = err_hand(text)
#		did an error accure?
		except:
#			tell the user that there were an error and ask him/her/it to send bug report
			outText = 'PRIVMSG '+ text.split(':')[1].split('!')[0] + ' :Unhandled error handled! Please send bug report to TheZulk!'
#		are there a message to be sent?
		if outText != '':
#			send the massage
			sock.send(outText + '\r\n')
#			print out what was sent
			print '<me> ' + outText
#	print out a message that the program should be stoped
	print 'Thread exiting... the program shouldn\'t be running now'

def connect(server,port):
	'''Connects to the specified server
will try untill connected'''
	global sock,iName,fString,curNick
#	set "again" to true
	again = 1
#	should we try (again)?
	while again:
#		catch errors
		try:
#			create the socket
			sock=socket(AF_INET,SOCK_STREAM)
#			connect to the server
			sock.connect((server,port))

#			send the NICK request
			sock.send('NICK '+curNick+'\r\n')
			print '<me> NICK '+curNick

#			send the USER string
			sock.send('USER '+iName+' p2.zlk '+server+' :'+fString+'\r\n')
			print '<me> USER '+iName+' p2.zlk '+server+' :'+fString
#			if no errors has accured we set "again" to false
			again = 0
#		did something go wrong?
		except:
#			say something is wrong, sleep 5 seconds and try again
			print 'Error connecting... waiting for 5 seconds'
			sleep(5)
			print 'reconnecting\n'

if __name__ == '__main__':
#	did someone specify a server?
	if len(argv) > 1:
#		set "server" to the specified server
		server = argv[1]
		print server

#	set "configFile" to the right configuration file
	configFile = '/opt/.zulk-bot'
#	does the configuration file exist?
	if exists(configFile):
#		load the configuration
		comList = pickle.load(file(configFile,'r'))
#	connect to the server
	connect(server,6667)
#	set "running" to true
	running = 1
#	start the message handling thread
	thread = _start_new_thread(read_sock, tuple())

#	get and send RAW IRC commands from STDIN to the IRC server
	while running:
		sock.send(raw_input()+'\r\n')
