dickserv

- irc bot with many useful commands
git clone git://git.acid.vegas/dickserv.git
Log | Files | Refs | Archive | README | LICENSE

irc.py (20156B)

      1 #!/usr/bin/env python
      2 # -*- coding: utf-8 -*-
      3 # DickServ IRC Bot - Developed by acidvegas in Python (https://acid.vegas/dickserv)
      4 # irc.py
      5 
      6 import socket
      7 import time
      8 
      9 import config
     10 import constants
     11 import database
     12 import debug
     13 import functions
     14 import httplib
     15 
     16 from commands import *
     17 
     18 # Load optional modules
     19 if config.connection.ssl:
     20 	import ssl
     21 if config.connection.proxy:
     22 	import sock
     23 
     24 def color(msg, foreground, background=None):
     25 	if foreground == 'random':
     26 		foreground = '{0:0>2}'.format(functions.random_int(2,13))
     27 	if background == 'random':
     28 		background = '{0:0>2}'.format(functions.random_int(2,13))
     29 	if background:
     30 		return f'\x03{foreground},{background}{msg}{constants.reset}'
     31 	else:
     32 		return f'\x03{foreground}{msg}{constants.reset}'
     33 
     34 class IRC(object):
     35 	def __init__(self):
     36 		self.last   = 0
     37 		self.slow	= False
     38 		self.sock	= None
     39 		self.start  = 0
     40 		self.status = True
     41 
     42 	def connect(self):
     43 		try:
     44 			self.create_socket()
     45 			self.sock.connect((config.connection.server, config.connection.port))
     46 			self.register()
     47 		except socket.error as ex:
     48 			debug.error('Failed to connect to IRC server.', ex)
     49 			Events.disconnect()
     50 		else:
     51 			self.listen()
     52 
     53 	def create_socket(self):
     54 		family = socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET
     55 		if config.connection.proxy:
     56 			proxy_server, proxy_port = config.connection.proxy.split(':')
     57 			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
     58 			self.sock.setblocking(0)
     59 			self.sock.settimeout(15)
     60 			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
     61 		else:
     62 			self.sock = socket.socket(family, socket.SOCK_STREAM)
     63 		if config.connection.vhost:
     64 			self.sock.bind((config.connection.vhost, 0))
     65 		if config.connection.ssl:
     66 			ctx = ssl.SSLContext()
     67 			if config.cert.file:
     68 				ctx.load_cert_chain(config.cert.file, config.cert.key, config.cert.password)
     69 			if config.connection.ssl_verify:
     70 				ctx.verify_mode = ssl.CERT_REQUIRED
     71 				ctx.load_default_certs()
     72 			else:
     73 				ctx.check_hostname = False
     74 				ctx.verify_mode	= ssl.CERT_NONE
     75 			self.sock = ctx.wrap_socket(self.sock)
     76 
     77 	def listen(self):
     78 		while True:
     79 			try:
     80 				data = self.sock.recv(1024).decode('utf-8')
     81 				for line in (line for line in data.split('\r\n') if line):
     82 					debug.irc(line)
     83 					if len(line.split()) >= 2:
     84 						Events.handle(line)
     85 			except (UnicodeDecodeError,UnicodeEncodeError):
     86 				pass
     87 			except Exception as ex:
     88 				debug.error('Unexpected error occured.', ex)
     89 				break
     90 		Events.disconnect()
     91 
     92 	def register(self):
     93 		if config.login.network:
     94 			Commands.raw('PASS ' + config.login.network)
     95 		Commands.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}')
     96 		Commands.nick(config.ident.nickname)
     97 
     98 
     99 
    100 class Commands:
    101 	def action(chan, msg):
    102 		Commands.sendmsg(chan, f'\x01ACTION {msg}\x01')
    103 
    104 	def error(target, data, reason=None):
    105 		if reason:
    106 			Commands.sendmsg(target, '[{0}] {1} {2}'.format(color('!', constants.red), data, color('({0})'.format(reason), constants.grey)))
    107 		else:
    108 			Commands.sendmsg(target, '[{0}] {1}'.format(color('!', constants.red), data))
    109 
    110 	def identify(nick, password):
    111 		Commands.sendmsg('nickserv', f'identify {nick} {password}')
    112 
    113 	def join_channel(chan, key=None):
    114 		Commands.raw(f'JOIN {chan} {key}') if key else Commands.raw('JOIN ' + chan)
    115 
    116 	def mode(target, mode):
    117 		Commands.raw(f'MODE {target} {mode}')
    118 
    119 	def nick(nick):
    120 		Commands.raw('NICK ' + nick)
    121 
    122 	def notice(target, msg):
    123 		Commands.raw(f'NOTICE {target} :{msg}')
    124 
    125 	def oper(user, password):
    126 		Commands.raw(f'OPER {user} {password}')
    127 
    128 	def raw(msg):
    129 		msg = msg.replace('\r','').replace('\n','')[:450]
    130 		DickServ.sock.send(bytes(msg + '\r\n', 'utf-8'))
    131 
    132 	def sendmsg(target, msg):
    133 		Commands.raw(f'PRIVMSG {target} :{msg}')
    134 
    135 
    136 
    137 class Events:
    138 	def connect():
    139 		DickServ.start = time.time()
    140 		if config.settings.modes:
    141 			Commands.mode(config.ident.nickname, '+' + config.settings.modes)
    142 		if config.login.nickserv:
    143 			Commands.identify(config.ident.nickname, config.login.nickserv)
    144 		if config.login.operator:
    145 			Commands.oper(config.ident.username, config.login.operator)
    146 		Commands.join_channel(config.connection.channel, config.connection.key)
    147 
    148 	def disconnect():
    149 		DickServ.sock.close()
    150 		time.sleep(10)
    151 		DickServ.connect()
    152 
    153 	def kick(nick, chan, kicked):
    154 		if kicked == config.ident.nickname and chan == config.connection.channel:
    155 			time.sleep(3)
    156 			Commands.join_channel(chan, config.connection.key)
    157 
    158 	def message(nick, ident, chan, msg):
    159 		try:
    160 			if chan == config.connection.channel and (DickServ.status or ident == config.settings.admin):
    161 				if not msg.startswith(config.settings.cmd_char):
    162 					urls = httplib.parse_urls(msg)
    163 					if urls:
    164 						if time.time() - DickServ.last > 3:
    165 							DickServ.last = time.time()
    166 							Events.url(chan, urls[0])
    167 					elif msg == '@dickserv':
    168 						Commands.sendmsg(chan, constants.bold + 'DickServ IRC Bot - Developed by acidvegas in Python - https://acid.vegas/dickserv')
    169 					elif msg == '@dickserv help':
    170 						Commands.sendmsg(chan, 'https://git.supernets.org/acidvegas/dickserv#commands')
    171 					elif msg == 'h' and functions.luck(4):
    172 						Commands.sendmsg(chan, 'h')
    173 					elif 'qhat' in msg:
    174 						Commands.sendmsg(chan, 'Q:)')
    175 				elif ident not in database.Ignore.idents():
    176 					if time.time() - DickServ.last < 3 and ident != config.settings.admin:
    177 						if not DickServ.slow:
    178 							Commands.sendmsg(chan, color('Slow down nerd!', constants.red))
    179 							DickServ.slow = True
    180 					else:
    181 						DickServ.slow = False
    182 						args = msg.split()
    183 						argz = msg[len(args[0])+1:]
    184 						cmd  = args[0][1:]
    185 						if len(args) == 1:
    186 							if cmd == 'date':
    187 								Commands.sendmsg(chan, functions.current_date())
    188 							elif cmd == 'talent':
    189 								if functions.luck(1000):
    190 									Commands.sendmsg(chan, color(f' !!! HOLY FUCKING SHIT {nick} ACHIEVED TALENT !!! ',               'random', 'random'))
    191 									Commands.sendmsg(chan, color(' !!! RIP DITTLE DIP DIP DIP DIP IT\'S YOUR BIRTHDAY !!! ',          'random', 'random'))
    192 									Commands.sendmsg(chan, color(f' !!! CAN WE HAVE A GOT DAMN MOMENT OF SILENCE FOR {nick} :) !!! ', 'random', 'random'))
    193 									Commands.sendmsg(chan, color(' !!! GOT DAMN XD THIS IS TOO CRAZY LIKE...DAMN HAHA. DAMN. !!! ',   'random', 'random'))
    194 								else:
    195 									Commands.sendmsg(chan, color('(^)', 'random'))
    196 							elif cmd == 'todo':
    197 								todos = database.Todo.read(ident)
    198 								if todos:
    199 									for item in todos:
    200 										Commands.notice(nick, '[{0}] {1}'.format(color(todos.index(item)+1, constants.pink), item))
    201 								else:
    202 									Commands.notice(nick, 'You have no saved todos.')
    203 							elif cmd == 'uptime':
    204 								Commands.sendmsg(chan, functions.uptime(DickServ.start))
    205 						elif len(args) == 2:
    206 							if cmd == 'coin':
    207 								api = cryptocurrency.get(args[1])
    208 								if api:
    209 									Commands.sendmsg(chan, '{0} {1} - ${2:,.2f}'.format(color(api['name'], constants.white), color('({0})'.format(api['symbol']), constants.grey), float(api['price_usd'])))
    210 								else:
    211 									Commands.error(chan, 'Invalid cryptocurrency name!')
    212 							elif cmd == 'drug':
    213 								api = tripsit.drug(args[1])
    214 								if api:
    215 									Commands.sendmsg(chan, '{0} - {1}'.format(color(api['name'], constants.yellow), api['desc']))
    216 								else:
    217 									Commands.error(chan, 'No results found.')
    218 							elif cmd == 'define':
    219 								definition = dictionary.define(args[1])
    220 								if definition:
    221 									Commands.sendmsg(chan, '{0} - {1}: {2}'.format(color('Definition', constants.white, constants.blue), args[1].lower(), definition))
    222 								else:
    223 									Commands.error(chan, 'No results found.')
    224 							elif cmd == 'isup':
    225 								Commands.sendmsg(chan, '{0} is {1}'.format(args[1], isup.check(args[1])))
    226 							elif cmd == 'r':
    227 								api = reddit.read(args[1])
    228 								if api:
    229 									data = list(api.keys())
    230 									for i in data:
    231 										count = str(data.index(i)+1)
    232 										Commands.sendmsg(chan, '[{0}] {1} [{2}|{3}/{4}|{5}]'.format(color(count, constants.pink), functions.trim(i, 100), color(str(api[i]['score']), constants.white), color('+' + str(api[i]['ups']), constants.green), color('-' + str(api[i]['downs']), constants.red), color(api[i]['comments'], constants.white)))
    233 										Commands.sendmsg(chan, ' - ' + color(api[i]['url'], constants.grey))
    234 								else:
    235 									Commands.error(chan, 'No results found.')
    236 							elif cmd == 'w':
    237 								if args[1].isdigit():
    238 									api = weather.lookup(args[1])
    239 									if api:
    240 										Commands.sendmsg(chan, api)
    241 									else:
    242 										Commands.error(chan, 'No results found.')
    243 								else:
    244 									Commands.error(chan, 'Invalid arguments.')
    245 						if len(args) >= 2:
    246 							if cmd == 'g':
    247 								api = google.search(argz, database.Settings.get('max_results'))
    248 								if api:
    249 									for result in api:
    250 										count = api.index(result)+1
    251 										Commands.sendmsg(chan, '[{0}] {1}'.format(color(count, constants.pink), result['title']))
    252 										Commands.sendmsg(chan, ' - ' + color(result['link'], constants.grey))
    253 								else:
    254 									Commands.error(chan, 'No results found.')
    255 							elif cmd == 'imdb':
    256 								api = imdb.search(argz)
    257 								if api:
    258 									Commands.sendmsg(chan, '{0} {1} {2} {3}'.format(color('Title  :', constants.white), api['Title'], api['Year'], color(api['Rated'], constants.grey)))
    259 									Commands.sendmsg(chan, '{0} {1}{2}'.format(color('Link   :', constants.white), constants.underline, color('https://imdb.com/title/' +  api['imdbID'], constants.light_blue)))
    260 									Commands.sendmsg(chan, '{0} {1}'.format(color('Genre  :', constants.white), api['Genre']))
    261 									if api['imdbRating'] == 'N/A':
    262 										Commands.sendmsg(chan, '{0} {1} 0/10'.format(color('Rating :', constants.white), color('★★★★★★★★★★', constants.grey)))
    263 									else:
    264 										Commands.sendmsg(chan, '{0} {1}{2} {3}/10'.format(color('Rating :', constants.white), color('★'*round(float(api['imdbRating'])), constants.yellow), color('★'*(10-round(float(api['imdbRating']))), constants.grey), api['imdbRating']))
    265 									Commands.sendmsg(chan, '{0} {1}'.format(color('Plot   :', constants.white), api['Plot']))
    266 								else:
    267 									Commands.error(chan, 'No results found.')
    268 							elif cmd == 'netsplit':
    269 								api = netsplit.search(argz)
    270 								if api:
    271 									data = list(api.keys())
    272 									for i in data:
    273 										count = str(data.index(i)+1)
    274 										Commands.sendmsg(chan, '[{0}] {1} {2} / {3}'.format(color(count, constants.pink), color(i, constants.light_blue), color('({0})'.format(api[i]['users']), constants.grey), color(api[i]['network'], constants.red)))
    275 										Commands.sendmsg(chan, color(' - ' + api[i]['topic'], constants.grey))
    276 								else:
    277 									Commands.error(chan, 'No results found.')
    278 							elif cmd == 'todo' and len(args) >= 3:
    279 								if len(args) >= 3 and args[1] == 'add':
    280 									todos = database.Todo.read(ident)
    281 									if len(todos) <= database.Settings.get('max_todo_per') and len(database.Todo.read()) <= database.Settings.get('max_todo'):
    282 										argz = argz[4:]
    283 										if argz not in todos:
    284 											database.Todo.add(functions.get_date(), ident, argz)
    285 											Commands.notice(nick, 'Todo added to database!')
    286 										else:
    287 											Commands.notice(nick, 'Todo already in database!')
    288 									else:
    289 										Commands.notice(nick, 'Maximum todos reached!')
    290 								elif len(args) == 3 and args[1] == 'del':
    291 									num = args[2]
    292 									if num.isdigit():
    293 										num   = int(num)
    294 										todos = database.Todo.read(ident)
    295 										if todos:
    296 											if num <= len(todos):
    297 												for item in todos:
    298 													count = todos.index(item)+1
    299 													if count == num:
    300 														database.Todo.remove(ident, item)
    301 														break
    302 												Commands.notice(nick, 'Todo removed from database!')
    303 											else:
    304 												Commands.notice(nick, 'Invalid number.')
    305 										else:
    306 											Commands.notice(nick, 'No todos found.')
    307 									else:
    308 										Commands.notice(nick, 'Invalid number.')
    309 								else:
    310 									Commands.notice(nick, 'Invalid arguments.')
    311 							elif cmd == 'tpb':
    312 								api = tpb.search(argz, database.Settings.get('max_results'))
    313 								if api:
    314 									data = list(api.keys())
    315 									for i in data:
    316 										count = str(data.index(i)+1)
    317 										Commands.sendmsg(chan, '[{0}] {1} [{2}/{3}]'.format(color(count, constants.pink), i, color(api[i]['seeders'], constants.green), color(api[i]['leechers'], constants.red)))
    318 										Commands.sendmsg(chan, ' - ' + color('http://thepiratebay.org' + api[i]['url'], constants.grey))
    319 								else:
    320 									Commands.error(chan, 'No results found.')
    321 							elif cmd == 'ud':
    322 								definition = dictionary.urban(argz)
    323 								if definition:
    324 									Commands.sendmsg(chan, '{0}{1} - {2}: {3}'.format(color('urban', constants.white, constants.blue), color('DICTIONARY', constants.yellow, constants.black), argz, definition))
    325 								else:
    326 									Commands.error(chan, 'No results found.')
    327 							elif cmd == 'wolfram':
    328 								results = wolfram.ask(argz)
    329 								if results:
    330 									Commands.sendmsg(chan, '{0}{1} - {2}'.format(color('Wolfram', constants.red), color('Alpha', constants.orange), results))
    331 								else:
    332 									Commands.error(chan, 'No results found.')
    333 							elif cmd == 'yt':
    334 								api  = youtube.search(argz, database.Settings.get('max_results'))
    335 								if api:
    336 									data = list(api.keys())
    337 									for i in api.keys():
    338 										count = str(data.index(i)+1)
    339 										Commands.sendmsg(chan, '[{0}] {1}'.format(color(count, constants.pink), functions.trim(i, 75)))
    340 										Commands.sendmsg(chan, ' - ' + color(api[i], constants.grey))
    341 								else:
    342 									Commands.error(chan, 'No results found.')
    343 						DickServ.last = time.time()
    344 		except Exception as ex:
    345 			Commands.error(chan, 'Command threw an exception.', ex)
    346 
    347 	def nick_in_use():
    348 		debug.error('DickServ is already running or nick is in use.')
    349 
    350 	def private(nick, ident, msg):
    351 		try:
    352 			if ident == config.settings.admin_host:
    353 				args = msg.split()
    354 				cmd  = args[0][1:]
    355 				if len(args) == 1:
    356 					if cmd == 'config':
    357 						settings = database.Settings.read()
    358 						Commands.sendmsg(nick, '[{0}]'.format(color('Settings', constants.purple)))
    359 						for setting in settings:
    360 							Commands.sendmsg(nick, '{0} = {1}'.format(color(setting[0], constants.yellow), color(setting[1], constants.grey)))
    361 					elif cmd == 'ignore':
    362 						ignores = database.Ignore.idents()
    363 						if ignores:
    364 							Commands.sendmsg(nick, '[{0}]'.format(color('Ignore List', constants.purple)))
    365 							for user in ignores:
    366 								Commands.sendmsg(nick, color(user, constants.yellow))
    367 							Commands.sendmsg(nick, '{0} {1}'.format(color('Total:', constants.light_blue), color(len(ignores), constants.grey)))
    368 						else:
    369 							Commands.error(nick, 'Ignore list is empty!')
    370 					elif cmd == 'off':
    371 						DickServ.status = False
    372 						Commands.sendmsg(nick, color('OFF', constants.red))
    373 					elif cmd == 'on':
    374 						DickServ.status = True
    375 						Commands.sendmsg(nick, color('ON', constants.green))
    376 				elif len(args) == 2:
    377 					if cmd == 'ignore' and args[1] == 'reset':
    378 						database.Ignore.reset()
    379 					elif cmd == 'todo' and args[1] == 'expire':
    380 						database.Todo.expire_check()
    381 					elif cmd == 'todo' and args[1] == 'reset':
    382 						database.Todo.reset()
    383 				elif len(args) == 3:
    384 					if cmd == 'config':
    385 						setting, value = args[1], args[2]
    386 						if functions.CheckString.number(value):
    387 							value = functions.floatint(value)
    388 							if value >= 0:
    389 								if setting in database.Settings.settings():
    390 									database.Settings.update(setting, value)
    391 									Commands.sendmsg(nick, 'Change setting for {0} to {1}.'.format(color(setting, constants.yellow), color(value, constants.grey)))
    392 								else:
    393 									Commands.error(nick, 'Invalid config variable.')
    394 							else:
    395 								Commands.error(nick, 'Value must be greater than or equal to zero.')
    396 						else:
    397 							Commands.error(nick, 'Value must be an integer or float.')
    398 					elif cmd == 'ignore':
    399 						if args[1] == 'add':
    400 							user_ident = args[2]
    401 							if user_ident not in database.Ignore.idents():
    402 								database.Ignore.add(nickname, user_ident)
    403 								Commands.sendmsg(nick, 'Ident {0} to the ignore list.'.format(color('added', constants.green)))
    404 							else:
    405 								Commands.error(nick, 'Ident is already on the ignore list.')
    406 						elif cmd == 'del':
    407 							user_ident = args[2]
    408 							if user_ident in database.Ignore.idents():
    409 								database.Ignore.remove(user_ident)
    410 								Commands.sendmsg(nick, 'Ident {0} from the ignore list.'.format(color('removed', constants.red)))
    411 							else:
    412 								Commands.error(nick, 'Ident does not exist in the ignore list.')
    413 		except Exception as ex:
    414 			Commands.error(nick, 'Command threw an exception.', ex)
    415 
    416 	def url(chan, url):
    417 		try:
    418 			if imdb.check(url):
    419 				id  = imdb.check(url)
    420 				api = imdb.search(id)
    421 				if api:
    422 					Commands.sendmsg(chan, '{0} {1} {2} {3}'.format(color('Title  :', constants.white), api['Title'], api['Year'], color(api['Rated'], constants.grey)))
    423 					Commands.sendmsg(chan, '{0} {1}{2}'.format(color('Link   :', constants.white), constants.underline, color('https://imdb.com/title/' +  api['imdbID'], constants.light_blue)))
    424 					Commands.sendmsg(chan, '{0} {1}'.format(color('Genre  :', constants.white), api['Genre']))
    425 					if api['imdbRating'] == 'N/A':
    426 						Commands.sendmsg(chan, '{0} {1} 0/10'.format(color('Rating :', constants.white), color('★★★★★★★★★★', constants.grey)))
    427 					else:
    428 						Commands.sendmsg(chan, '{0} {1}{2} {3}/10'.format(color('Rating :', constants.white), color('★'*round(float(api['imdbRating'])), constants.yellow), color('★'*(10-round(float(api['imdbRating']))), constants.grey), api['imdbRating']))
    429 					Commands.sendmsg(chan, '{0} {1}'.format(color('Plot   :', constants.white), api['Plot']))
    430 			elif reddit.check(url):
    431 				subreddit = reddit.check(url)[0]
    432 				post_id   = reddit.check(url)[1]
    433 				api       = reddit.post_info(subreddit, post_id)
    434 				if api:
    435 					Commands.sendmsg(chan, '[{0}] - {1} [{2}|{3}/{4}|{5}]'.format(color('reddit', constants.cyan), color(functions.trim(api['title'], 75), constants.white), color(api['score'], constants.white), color('+' + api['ups'], constants.green), color('-' + api['downs'], constants.red), color(api['num_comments'], constants.white)))
    436 			elif youtube.check(url):
    437 				api = youtube.video_info(youtube.check(url))
    438 				if api:
    439 					Commands.sendmsg(chan, '{0}{1} - {2} [{3}|{4}/{5}]'.format(color('You', constants.black, constants.white), color('Tube', constants.white, constants.red), functions.trim(api['title'], 75), color(api['views'], constants.white), color('+' + api['likes'], constants.green), color('-' + api['dislikes'], constants.red)))
    440 					Commands.sendmsg(chan, color(api['description'], constants.grey))
    441 			else:
    442 				url_type = httplib.get_type(url)
    443 				if url_type == 'text/html':
    444 					title = httplib.get_title(url)
    445 					Commands.sendmsg(chan, '[{0}] {1}'.format(color(url_type, constants.pink), color(title, constants.white)))
    446 				else:
    447 					file_name = httplib.get_file(url)
    448 					if file_name:
    449 						file_size = httplib.get_size(url)
    450 						Commands.sendmsg(chan, '[{0}] {1} [{2}]'.format(color(url_type, constants.pink), color(file_name, constants.white), color(file_size, constants.blue)))
    451 		except Exception as ex:
    452 			debug.error('Title Error', ex)
    453 
    454 	def handle(data):
    455 		args = data.split()
    456 		if data.startswith('ERROR :Closing Link:'):
    457 			raise Exception('Connection has closed.')
    458 		elif args[0] == 'PING':
    459 			Commands.raw('PONG ' + args[1][1:])
    460 		elif args[1] == constants.RPL_WELCOME:
    461 			Events.connect()
    462 		elif args[1] == constants.ERR_NICKNAMEINUSE:
    463 			Events.nick_in_use()
    464 		elif args[1] == constants.KICK:
    465 			nick   = args[0].split('!')[0][1:]
    466 			chan   = args[2]
    467 			kicked = args[3]
    468 			Events.kick(nick, chan, kicked)
    469 		elif args[1] == constants.PRIVMSG:
    470 			nick  = args[0].split('!')[0][1:]
    471 			ident = args[0].split('!')[1]
    472 			chan  = args[2]
    473 			msg   = ' '.join(args[3:])[1:]
    474 			if chan == config.ident.nickname:
    475 				Events.private(nick, ident, msg)
    476 			else:
    477 				Events.message(nick, ident, chan, msg)
    478 
    479 DickServ = IRC()