wm: pyircd

ref: 34ba346d0d82fa4e34e953f872d62bfc5c375df0
dir: /ircd.py/

View raw version
#!/usr/bin/env python
# a basic ircd
# JOIN, PART
import socket
import time
from select import select
from enum import Enum

defaults = {}
defaults['hostname'] = 'localhost'
defaults['pingtime'] = 250
defaults['name'] = 'ircd.py'
defaults['version'] = '1.0'
defaults['host'] = '' # aka 0.0.0.0
defaults['port'] = 6667
defaults['motd'] = 'Hi.'
defaults['encoding'] = 'utf-8'

# codes
class C(Enum):
	WELCOME = 1
	
	MOTD = 372
	MOTDSTART = 375
	ENDOFMOTD = 376
	

	# errors
	NOSUCHNICK = 401
	
	UNKNOWNCOMMAND = 421
	
	ERRONEUSNICKNAME = 431
	NONICKNAMEGIVEN = 432
	NICKNAMEINUSE = 433
	
	NOTONCHANNEL = 442
	
	#NOTREGISTERED = 451
	
	NEEDMOREPARAMS = 461
	ALREADYREGISTERED = 462

Cmsg = {}
Cmsg[C.WELCOME] = 'Welcome.'
Cmsg[C.MOTDSTART] = 'Message of the day is:'
Cmsg[C.ENDOFMOTD] = 'End of the /MOTD'
Cmsg[C.NOSUCHNICK] = 'No such nickname or channel'
Cmsg[C.UNKNOWNCOMMAND] = 'Unknown command'
Cmsg[C.ERRONEUSNICKNAME] = 'Invalid nick'
Cmsg[C.NONICKNAMEGIVEN] = 'No nickname were given'
Cmsg[C.NICKNAMEINUSE] = 'Nickname is already in use'
Cmsg[C.NOTONCHANNEL] = 'No such channel'
Cmsg[C.NEEDMOREPARAMS] = 'Not enough parameters'
Cmsg[C.ALREADYREGISTERED] = 'Client is already registered'

class Client(object):
	def __init__(self, server, sock, addr):
		self.sock = sock
		self.addr = addr
		self.server = server
		
		self.ip = self.addr[0]
		self.port = self.addr[1]
		
		self.nick = ''
		self.user = ''
		self.real = ''
		self.registered = False
		
		self.ping = 0
		
		self.sendbuf = []
		self.recvbuf = []

		print('New client', self.ip, self.port, 'has connected')

	def send(self, msg):
		self.sendbuf.append(msg)

	def _reply(self, cmd, msg):
		self.send(':{} {} {} {}\r\n'.format(self.server.hostname, str(cmd).rjust(3, '0'), self.nick, msg))

	def reply(self, cmd):
		self._reply(cmd.value, Cmsg[cmd])
		
	def register(self):
		if(self.nick == '' or self.user == '' or self.registered != False):
			return False
		
		self.registered = True
		print('Client', self.ip, 'has registed with nick', self.nick, 'and user', self.user)
		self.reply(C.WELCOME)
		self.cmd_motd([])
		return True
	
	def cmd_handler(self, cmdl):
		print('cmdl:', cmdl)
		
		if(len(cmdl) != 0 and cmdl[-1] == '\r'):
			cmdl = cmdl[:-1]
			
		cmdl = cmdl.split(' ')
		cmd = cmdl[0]
		if(len(cmdl) > 0):
			args = cmdl[1:]
		else:
			args = ''
		
		# fast exit
		if(cmd == '\r' or cmd == '\n' or cmd == '\r\n' or cmd.isspace() or cmd == ''):
			return
		
		print('cmd:', cmd)
		print('args:', args)
		
		match(cmd.upper()):
			case 'NICK':
				self.cmd_nick(args)
			case 'USER':
				self.cmd_user(args)
			case 'PING':
				self.cmd_ping(args)
			case 'PRIVMSG':
				self.cmd_privmsg(args)
			case 'MOTD':
				self.cmd_motd(args)
			case 'QUIT':
				self.cmd_quit(args)
			case 'JOIN':
				self.cmd_join(args)
			case 'PART':
				self.cmd_part(args)
			case _:
				self._reply(C.UNKNOWNCOMMAND.value, Cmsg[C.UNKNOWNCOMMAND] + ' ' + cmd)

	def validnick(self, nick):
		return nick.isalpha()

	def cmd_nick(self, nick):
		if(len(nick) < 1 or nick[0] == ''):
			self.reply(C.NONICKNAMEGIVEN)
		
		elif(self.validnick(nick[0]) == False):
			self.reply(C.ERRONEUSNICKNAME)
		
		elif(self.server.findnick(nick[0]) != None):
			self.reply(C.NICKNAMEINUSE)
		else:
			self.nick = nick[0]
		
		self.register()
		
	def cmd_user(self, user):
		if(self.user != ''):
			self.reply(C.ALREADYREGISTERED)
		else:
			self.user = user[0]
		
		self.register()
	
	def cmd_ping(self, cmd):
		id = cmd[0]
		self.send('PONG {}\n'.format(id))
	
	def cmd_quit(self, cmd):
		# XXX self.broadcast()
		self.sock.send(bytes('ERROR :closing link\r\n', 'utf-8'))
		self.sock.close()
		self.server.clients.remove(self)
	
	def cmd_privmsg(self, cmd):
		if(len(cmd) < 2):
			self.reply(C.NEEDMOREPARAMS)
		
		dst = cmd[0]
		msg = cmd[1:]
		
		tmsg = ''
		for i in msg:
			tmsg += (' ' + i)
		
		u = self.server.findnick(dst)
		ch = self.server.findchan(dst)
		if(dst[0] == '#'):
			if(ch == None):
				self.reply(C.NOSUCHNICK)
			else:
				ch.bcast(':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
			
		else:
			if(u == None):
				self.reply(C.NOSUCHNICK)
			else:
				u.send(':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
				#u.send(':%s PRIVMSG %s%s\r\n' % (self.nick, nick, tmsg))
		
		print('privmsg(): ', ':%s!%s@%s PRIVMSG %s%s\r\n' % (self.nick, self.user, self.ip, dst, tmsg))
	
	def cmd_motd(self, cmd):
		self.reply(C.MOTDSTART)
		self._reply(C.MOTD.value, self.server.motd)
		self.reply(C.ENDOFMOTD)
	
	def cmd_join(self, cmd):
		if(len(cmd) < 1):
			self.reply(C.NEEDMOREPARAMS)
		
		chan = cmd[0]
		channel = self.server.findchan(chan)
		
		if(channel == None):
			channel = self.server.chans.append(Chan(chan, self))
		else:
			channel.clients.append(self)
		
	def cmd_part(self, cmd):
		if(len(cmd) < 1):
			self.reply(C.NEEDMOREPARAMS)
		chan = cmd[0]
		channel = self.server.findchan(chan)
		
		if(not self in channel.clients):
			self.reply(C.NOTONCHANNEL)
		else:
			channel.clients.remove(self)
	
	def fileno(self):
		return self.sock.fileno()
	
class Chan(object):
	def __init__(self, name, client):
		self.name = name
		self.clients = [client]
		self.topic = ''
		self.ctime = int(time.time())

	def __repr__(self):
		return '<Channel ' + self.name + '>'

	def bcast(self, msg):
		for c in self.clients:
			c.sendbuf.append(msg)
	
	def send(self, client, msg):
		for c in self.clients:
			if(c == client):
				return
			c.sendbuf.append(msg)
	
		
class Server(socket.socket):
	def __init__(self, cfg):
		super(Server, self).__init__(socket.AF_INET, socket.SOCK_STREAM)
		self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		
		self.chans = []
		self.clients = []

		self.name = cfg['name']
		self.motd = cfg['motd']

		self.hostname = cfg['hostname']
		self.pingtime = cfg['pingtime']

		self.host = cfg['host']
		self.port = cfg['port']
		
		self.encoding = cfg['encoding']
	
	
	def findnick(self, nick):
		for i in self.clients:
			if(i.nick == nick):
				return i
		return None

	def findchan(self, chan):
		for i in self.chans:
			if(i.name == chan):
				return i
		return None
	
	def main(self):
		self.bind((self.host, self.port))
		self.listen()

		while True:
			sendu = [c for c in self.clients if len(c.sendbuf) > 0]
			read, write, err = select([self] + self.clients, sendu, self.clients) # why self + self.clients

			# new clients
			if self in read:
				sock, address = self.accept()
				self.clients.append(Client(self, sock, address))
				continue

			for c in read:
				recv = c.sock.recv(4096)
				recv = recv.decode()
				recv = recv.strip('\r')
				
				print('client:', c.ip, 'recv:', recv)
				for i in recv.split('\n'):
					c.recvbuf.append(i)
					
				print('c.recvbuf:', c.recvbuf)
				
				while len(c.recvbuf) > 0:
					c.cmd_handler(c.recvbuf.pop(0))

			# send messages to each user
			for c in write:
				# send unsent parts of message each time
				while len(c.sendbuf) > 0:
					msg = bytes(c.sendbuf.pop(0), self.encoding)
					c.sock.send(msg)
		

def main():
	srv = Server(defaults)
	srv.main()

if __name__ == '__main__':
	main()