←︎ skeleton :: 4a96baa


1
commit 4a96baa37842bbd27d977662da14d071898106eb (HEAD -> master)
2
Author: acidvegas <acid.vegas@acid.vegas>
3
Date:   Fri Jun 28 02:23:40 2019 -0400
4
5
    Initial commit
6
---
7
 LICENSE                       |  15 +++
8
 README.md                     |  12 ++
9
 skeleton.py                   | 275 ++++++++++++++++++++++++++++++++++++++++
10
 skeleton/core/config.py       |  40 ++++++
11
 skeleton/core/constants.py    | 229 ++++++++++++++++++++++++++++++++++
12
 skeleton/core/database.py     |  35 ++++++
13
 skeleton/core/debug.py        |  81 ++++++++++++
14
 skeleton/core/functions.py    |  10 ++
15
 skeleton/core/irc.py          | 283 ++++++++++++++++++++++++++++++++++++++++++
16
 skeleton/data/cert/.gitignore |   4 +
17
 skeleton/data/logs/.gitignore |   4 +
18
 skeleton/modules/.gitignore   |   4 +
19
 skeleton/skeleton.py          |  24 ++++
20
 13 files changed, 1016 insertions(+)
21
22
diff --git a/LICENSE b/LICENSE
23
new file mode 100644
24
index 0000000..b63b809
25
--- /dev/null
26
+++ b/LICENSE
27
@@ -0,0 +1,15 @@
28
+ISC License
29
+
30
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
31
+
32
+Permission to use, copy, modify, and/or distribute this software for any
33
+purpose with or without fee is hereby granted, provided that the above
34
+copyright notice and this permission notice appear in all copies.
35
+
36
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
37
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
38
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
39
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
40
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
41
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
42
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
43
diff --git a/README.md b/README.md
44
new file mode 100644
45
index 0000000..5929ca7
46
--- /dev/null
47
+++ b/README.md
48
@@ -0,0 +1,12 @@
49
+###### Requirements
50
+* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)*
51
+* [PySocks](https://pypi.python.org/pypi/PySocks) *(**Optional:** For using the `proxy` setting)*
52
+
53
+###### IRC RCF Reference
54
+- http://www.irchelp.org/protocol/rfc/
55
+
56
+###### Mirrors
57
+- [acid.vegas](https://acid.vegas/skeleton) *(main)*
58
+- [SuperNETs](https://git.supernets.org/acidvegas/skeleton)
59
+- [GitHub](https://github.com/acidvegas/skeleton)
60
+- [GitLab](https://gitlab.com/acidvegas/skeleton)
61
diff --git a/skeleton.py b/skeleton.py
62
new file mode 100644
63
index 0000000..2324a3d
64
--- /dev/null
65
+++ b/skeleton.py
66
@@ -0,0 +1,275 @@
67
+#!/usr/bin/env python
68
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
69
+
70
+import socket
71
+import time
72
+import threading
73
+
74
+# Configuration
75
+_connection = {'server':'irc.server.com', 'port':6697, 'proxy':None, 'ssl':True, 'ssl_verify':False, 'ipv6':False, 'vhost':None}
76
+_cert       = {'file':None, 'key':None, 'password':None}
77
+_ident      = {'nickname':'DevBot', 'username':'dev', 'realname':'acid.vegas/skeleton'}
78
+_login      = {'nickserv':None, 'network':None, 'operator':None}
79
+_settings   = {'channel':'#dev', 'key':None, 'modes':None, 'throttle':1}
80
+
81
+# Formatting Control Characters / Color Codes
82
+bold        = '\x02'
83
+italic      = '\x1D'
84
+underline   = '\x1F'
85
+reverse     = '\x16'
86
+reset       = '\x0f'
87
+white       = '00'
88
+black       = '01'
89
+blue        = '02'
90
+green       = '03'
91
+red         = '04'
92
+brown       = '05'
93
+purple      = '06'
94
+orange      = '07'
95
+yellow      = '08'
96
+light_green = '09'
97
+cyan        = '10'
98
+light_cyan  = '11'
99
+light_blue  = '12'
100
+pink        = '13'
101
+grey        = '14'
102
+light_grey  = '15'
103
+
104
+def color(msg, foreground, background=None):
105
+	if background:
106
+		return f'\x03{foreground},{background}{msg}{reset}'
107
+	else:
108
+		return f'\x03{foreground}{msg}{reset}'
109
+
110
+def debug(msg):
111
+	print(f'{get_time()} | [~] - {msg}')
112
+
113
+def error(msg, reason=None):
114
+	if reason:
115
+		print(f'{get_time()} | [!] - {msg} ({reason})')
116
+	else:
117
+		print(f'{get_time()} | [!] - {msg}')
118
+
119
+def error_exit(msg):
120
+	raise SystemExit(f'{get_time()} | [!] - {msg}')
121
+
122
+def get_time():
123
+	return time.strftime('%I:%M:%S')
124
+
125
+class IRC(object):
126
+	def __init__(self):
127
+		self._queue = list()
128
+		self._sock = None
129
+
130
+	def _run(self):
131
+		Loop._loops()
132
+		self._connect()
133
+
134
+	def _connect(self):
135
+		try:
136
+			self._create_socket()
137
+			self._sock.connect((_connection['server'], _connection['port']))
138
+			self._register()
139
+		except socket.error as ex:
140
+			error('Failed to connect to IRC server.', ex)
141
+			Event._disconnect()
142
+		else:
143
+			self._listen()
144
+
145
+	def _create_socket(self):
146
+		family = socket.AF_INET6 if _connection['ipv6'] else socket.AF_INET
147
+		if _connection['proxy']:
148
+			proxy_server, proxy_port = _connection['proxy'].split(':')
149
+			self._sock = socks.socksocket(family, socket.SOCK_STREAM)
150
+			self._sock.setblocking(0)
151
+			self._sock.settimeout(15)
152
+			self._sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
153
+		else:
154
+			self._sock = socket.socket(family, socket.SOCK_STREAM)
155
+		if _connection['vhost']:
156
+			self._sock.bind((_connection['vhost'], 0))
157
+		if _connection['ssl']:
158
+			ctx = ssl.SSLContext()
159
+			if _cert['file']:
160
+				ctx.load_cert_chain(_cert['file'], _cert['key'], _cert['password'])
161
+			if _connection['ssl_verify']:
162
+				ctx.verify_mode = ssl.CERT_REQUIRED
163
+				ctx.load_default_certs()
164
+			else:
165
+				ctx.check_hostname = False
166
+				ctx.verify_mode = ssl.CERT_NONE
167
+			self._sock = ctx.wrap_socket(self._sock)
168
+
169
+	def _listen(self):
170
+		while True:
171
+			try:
172
+				data = self._sock.recv(1024).decode('utf-8')
173
+				for line in (line for line in data.split('\r\n') if len(line.split()) >= 2):
174
+					debug(line)
175
+					Event._handle(line)
176
+			except (UnicodeDecodeError,UnicodeEncodeError):
177
+				pass
178
+			except Exception as ex:
179
+				error('Unexpected error occured.', ex)
180
+				break
181
+		Event._disconnect()
182
+
183
+	def _register(self):
184
+		if _login['network']:
185
+			Bot._queue.append('PASS ' + _login['network'])
186
+		Bot._queue.append('USER {0} 0 * :{1}'.format(_ident['username'], _ident['realname']))
187
+		Bot._queue.append('NICK ' + _ident['nickname'])
188
+
189
+class Command:
190
+	def _action(target, msg):
191
+		Bot._queue.append(chan, f'\x01ACTION {msg}\x01')
192
+
193
+	def _ctcp(target, data):
194
+		Bot._queue.append(target, f'\001{data}\001')
195
+
196
+	def _invite(nick, chan):
197
+		Bot._queue.append(f'INVITE {nick} {chan}')
198
+
199
+	def _join(chan, key=None):
200
+		Bot._queue.append(f'JOIN {chan} {key}') if key else Bot._queue.append('JOIN ' + chan)
201
+
202
+	def _mode(target, mode):
203
+		Bot._queue.append(f'MODE {target} {mode}')
204
+
205
+	def _nick(nick):
206
+		Bot._queue.append('NICK ' + nick)
207
+
208
+	def _notice(target, msg):
209
+		Bot._queue.append(f'NOTICE {target} :{msg}')
210
+
211
+	def _part(chan, msg=None):
212
+		Bot._queue.append(f'PART {chan} {msg}') if msg else Bot._queue.append('PART ' + chan)
213
+
214
+	def _quit(msg=None):
215
+		Bot._queue.append('QUIT :' + msg) if msg else Bot._queue.append('QUIT')
216
+
217
+	def _raw(data):
218
+		Bot._sock.send(bytes(data[:510] + '\r\n', 'utf-8'))
219
+
220
+	def _sendmsg(target, msg):
221
+		Bot._queue.append(f'PRIVMSG {target} :{msg}')
222
+
223
+	def _topic(chan, text):
224
+		Bot._queue.append(f'TOPIC {chan} :{text}')
225
+
226
+class Event:
227
+	def _connect():
228
+		if _settings['modes']:
229
+			Command._mode(_ident['nickname'], '+' + _settings['modes'])
230
+		if _login['nickserv']:
231
+			Command._sendmsg('NickServ', 'IDENTIFY {0} {1}'.format(_ident['nickname'], _login['nickserv']))
232
+		if _login['operator']:
233
+			Bot._queue.append('OPER {0} {1}'.format(_ident['username'], _login['operator']))
234
+		Command._join(_setting['channel'], _settings['key'])
235
+
236
+	def _ctcp(nick, chan, msg):
237
+		pass
238
+
239
+	def _disconnect():
240
+		Bot._sock.close()
241
+		Bot._queue = list()
242
+		time.sleep(15)
243
+		Bot._connect()
244
+
245
+	def _invite(nick, chan):
246
+		pass
247
+
248
+	def _join(nick, chan):
249
+		pass
250
+
251
+	def _kick(nick, chan, kicked):
252
+		if kicked == _ident['nickname'] and chan == _settings['channel']:
253
+			time.sleep(3)
254
+			Command.join(chan, _Settings['key'])
255
+
256
+	def _message(nick, chan, msg):
257
+		if msg == '!test':
258
+			Bot._queue.append(chan, 'It Works!')
259
+
260
+	def _nick_in_use():
261
+		error_exit('The bot is already running or nick is in use!')
262
+
263
+	def _part(nick, chan):
264
+		pass
265
+
266
+	def _private(nick, msg):
267
+		pass
268
+
269
+	def _quit(nick):
270
+		pass
271
+
272
+	def _handle(data):
273
+		args = data.split()
274
+		if data.startswith('ERROR :Closing Link:'):
275
+			raise Exception('Connection has closed.')
276
+		elif data.startswith('ERROR :Reconnecting too fast, throttled.'):
277
+			raise Exception('Connection has closed. (throttled)')
278
+		elif args[0] == 'PING':
279
+			Command._raw('PONG ' + args[1][1:])
280
+		elif args[1] == '001':
281
+			Event._connect()
282
+		elif args[1] == '433':
283
+			Event._nick_in_use()
284
+		elif args[1] == 'INVITE':
285
+			nick = args[0].split('!')[0][1:]
286
+			chan = args[3][1:]
287
+			Event._invite(nick, chan)
288
+		elif args[1] == 'JOIN':
289
+			nick = args[0].split('!')[0][1:]
290
+			chan = args[2][1:]
291
+			Event._join(nick, chan)
292
+		elif args[1] == 'KICK':
293
+			nick   = args[0].split('!')[0][1:]
294
+			chan   = args[2]
295
+			kicked = args[3]
296
+			Event._kick(nick, chan, kicked)
297
+		elif args[1] == 'PART':
298
+			nick = args[0].split('!')[0][1:]
299
+			chan = args[2]
300
+			Event._part(nick, chan)
301
+		elif args[1] == 'PRIVMSG':
302
+			#ident = args[0][1:]
303
+			nick   = args[0].split('!')[0][1:]
304
+			chan   = args[2]
305
+			msg    = ' '.join(args[3:])[1:]
306
+			if msg.startswith('\001'):
307
+				Event._ctcp(nick, chan, msg)
308
+			elif chan == _ident['nickname']:
309
+				Event._private(nick, msg)
310
+			else:
311
+				Event._message(nick, chan, msg)
312
+		elif args[1] == 'QUIT':
313
+			nick = args[0].split('!')[0][1:]
314
+			Event._quit(nick)
315
+
316
+class Loop:
317
+	def _loops():
318
+		threading.Thread(target=Loop._queue).start()
319
+
320
+	def _queue():
321
+		while True:
322
+			try:
323
+				if Bot._queue:
324
+					Command._raw(Bot._queue.pop(0))
325
+			except Exception as ex:
326
+				error('Error occured in the queue handler!', ex)
327
+			finally:
328
+				time.sleep(_settings['throttle'])
329
+
330
+# Main
331
+if _connection['proxy']:
332
+	try:
333
+		import socks
334
+	except ImportError:
335
+		error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)')
336
+if _connection['ssl']:
337
+	import ssl
338
+else:
339
+	del _cert, _connection['verify']
340
+Bot = IRC()
341
+Bot._run()
342
diff --git a/skeleton/core/config.py b/skeleton/core/config.py
343
new file mode 100644
344
index 0000000..0bb989c
345
--- /dev/null
346
+++ b/skeleton/core/config.py
347
@@ -0,0 +1,40 @@
348
+#!/usr/bin/env python
349
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
350
+# config.py
351
+
352
+class connection:
353
+	server     = 'irc.server.com'
354
+	port       = 6667
355
+	proxy      = None
356
+	ipv6       = False
357
+	ssl        = False
358
+	ssl_verify = False
359
+	vhost      = None
360
+	channel    = '#dev'
361
+	key        = None
362
+
363
+class cert:
364
+	key      = None
365
+	file     = None
366
+	password = None
367
+
368
+class ident:
369
+	nickname = 'skeleton'
370
+	username = 'skeleton'
371
+	realname = 'acid.vegas/skeleton'
372
+
373
+class login:
374
+	network  = None
375
+	nickserv = None
376
+	operator = None
377
+
378
+class throttle:
379
+	command   = 3
380
+	reconnect = 10
381
+	rejoin    = 3
382
+
383
+class settings:
384
+	admin    = 'nick!user@host.name' # Must be in nick!user@host format (Can use wildcards here)
385
+	cmd_char = '!'
386
+	log      = False
387
+	modes    = None
388
diff --git a/skeleton/core/constants.py b/skeleton/core/constants.py
389
new file mode 100644
390
index 0000000..3ffbdb8
391
--- /dev/null
392
+++ b/skeleton/core/constants.py
393
@@ -0,0 +1,229 @@
394
+#!/usr/bin/env python
395
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
396
+# constants.py
397
+
398
+# Control Characters
399
+bold      = '\x02'
400
+color     = '\x03'
401
+italic    = '\x1D'
402
+underline = '\x1F'
403
+reverse   = '\x16'
404
+reset     = '\x0f'
405
+
406
+# Color Codes
407
+white       = '00'
408
+black       = '01'
409
+blue        = '02'
410
+green       = '03'
411
+red         = '04'
412
+brown       = '05'
413
+purple      = '06'
414
+orange      = '07'
415
+yellow      = '08'
416
+light_green = '09'
417
+cyan        = '10'
418
+light_cyan  = '11'
419
+light_blue  = '12'
420
+pink        = '13'
421
+grey        = '14'
422
+light_grey  = '15'
423
+
424
+# Events
425
+PASS     = 'PASS'
426
+NICK     = 'NICK'
427
+USER     = 'USER'
428
+OPER     = 'OPER'
429
+MODE     = 'MODE'
430
+SERVICE  = 'SERVICE'
431
+QUIT     = 'QUIT'
432
+SQUIT    = 'SQUIT'
433
+JOIN     = 'JOIN'
434
+PART     = 'PART'
435
+TOPIC    = 'TOPIC'
436
+NAMES    = 'NAMES'
437
+LIST     = 'LIST'
438
+INVITE   = 'INVITE'
439
+KICK     = 'KICK'
440
+PRIVMSG  = 'PRIVMSG'
441
+NOTICE   = 'NOTICE'
442
+MOTD     = 'MOTD'
443
+LUSERS   = 'LUSERS'
444
+VERSION  = 'VERSION'
445
+STATS    = 'STATS'
446
+LINKS    = 'LINKS'
447
+TIME     = 'TIME'
448
+CONNECT  = 'CONNECT'
449
+TRACE    = 'TRACE'
450
+ADMIN    = 'ADMIN'
451
+INFO     = 'INFO'
452
+SERVLIST = 'SERVLIST'
453
+SQUERY   = 'SQUERY'
454
+WHO      = 'WHO'
455
+WHOIS    = 'WHOIS'
456
+WHOWAS   = 'WHOWAS'
457
+KILL     = 'KILL'
458
+PING     = 'PING'
459
+PONG     = 'PONG'
460
+ERROR    = 'ERROR'
461
+AWAY     = 'AWAY'
462
+REHASH   = 'REHASH'
463
+DIE      = 'DIE'
464
+RESTART  = 'RESTART'
465
+SUMMON   = 'SUMMON'
466
+USERS    = 'USERS'
467
+WALLOPS  = 'WALLOPS'
468
+USERHOST = 'USERHOST'
469
+ISON     = 'ISON'
470
+
471
+# Event Numerics
472
+RPL_WELCOME          = '001'
473
+RPL_YOURHOST         = '002'
474
+RPL_CREATED          = '003'
475
+RPL_MYINFO           = '004'
476
+RPL_ISUPPORT         = '005'
477
+RPL_TRACELINK        = '200'
478
+RPL_TRACECONNECTING  = '201'
479
+RPL_TRACEHANDSHAKE   = '202'
480
+RPL_TRACEUNKNOWN     = '203'
481
+RPL_TRACEOPERATOR    = '204'
482
+RPL_TRACEUSER        = '205'
483
+RPL_TRACESERVER      = '206'
484
+RPL_TRACESERVICE     = '207'
485
+RPL_TRACENEWTYPE     = '208'
486
+RPL_TRACECLASS       = '209'
487
+RPL_STATSLINKINFO    = '211'
488
+RPL_STATSCOMMANDS    = '212'
489
+RPL_STATSCLINE       = '213'
490
+RPL_STATSILINE       = '215'
491
+RPL_STATSKLINE       = '216'
492
+RPL_STATSYLINE       = '218'
493
+RPL_ENDOFSTATS       = '219'
494
+RPL_UMODEIS          = '221'
495
+RPL_SERVLIST         = '234'
496
+RPL_SERVLISTEND      = '235'
497
+RPL_STATSLLINE       = '241'
498
+RPL_STATSUPTIME      = '242'
499
+RPL_STATSOLINE       = '243'
500
+RPL_STATSHLINE       = '244'
501
+RPL_LUSERCLIENT      = '251'
502
+RPL_LUSEROP          = '252'
503
+RPL_LUSERUNKNOWN     = '253'
504
+RPL_LUSERCHANNELS    = '254'
505
+RPL_LUSERME          = '255'
506
+RPL_ADMINME          = '256'
507
+RPL_ADMINLOC1        = '257'
508
+RPL_ADMINLOC2        = '258'
509
+RPL_ADMINEMAIL       = '259'
510
+RPL_TRACELOG         = '261'
511
+RPL_TRYAGAIN         = '263'
512
+RPL_NONE             = '300'
513
+RPL_AWAY             = '301'
514
+RPL_USERHOST         = '302'
515
+RPL_ISON             = '303'
516
+RPL_UNAWAY           = '305'
517
+RPL_NOWAWAY          = '306'
518
+RPL_WHOISUSER        = '311'
519
+RPL_WHOISSERVER      = '312'
520
+RPL_WHOISOPERATOR    = '313'
521
+RPL_WHOWASUSER       = '314'
522
+RPL_ENDOFWHO         = '315'
523
+RPL_WHOISIDLE        = '317'
524
+RPL_ENDOFWHOIS       = '318'
525
+RPL_WHOISCHANNELS    = '319'
526
+RPL_LIST             = '322'
527
+RPL_LISTEND          = '323'
528
+RPL_CHANNELMODEIS    = '324'
529
+RPL_NOTOPIC          = '331'
530
+RPL_TOPIC            = '332'
531
+RPL_INVITING         = '341'
532
+RPL_INVITELIST       = '346'
533
+RPL_ENDOFINVITELIST  = '347'
534
+RPL_EXCEPTLIST       = '348'
535
+RPL_ENDOFEXCEPTLIST  = '349'
536
+RPL_VERSION          = '351'
537
+RPL_WHOREPLY         = '352'
538
+RPL_NAMREPLY         = '353'
539
+RPL_LINKS            = '364'
540
+RPL_ENDOFLINKS       = '365'
541
+RPL_ENDOFNAMES       = '366'
542
+RPL_BANLIST          = '367'
543
+RPL_ENDOFBANLIST     = '368'
544
+RPL_ENDOFWHOWAS      = '369'
545
+RPL_INFO             = '371'
546
+RPL_MOTD             = '372'
547
+RPL_ENDOFINFO        = '374'
548
+RPL_MOTDSTART        = '375'
549
+RPL_ENDOFMOTD        = '376'
550
+RPL_YOUREOPER        = '381'
551
+RPL_REHASHING        = '382'
552
+RPL_YOURESERVICE     = '383'
553
+RPL_TIME             = '391'
554
+RPL_USERSSTART       = '392'
555
+RPL_USERS            = '393'
556
+RPL_ENDOFUSERS       = '394'
557
+RPL_NOUSERS          = '395'
558
+ERR_NOSUCHNICK       = '401'
559
+ERR_NOSUCHSERVER     = '402'
560
+ERR_NOSUCHCHANNEL    = '403'
561
+ERR_CANNOTSENDTOCHAN = '404'
562
+ERR_TOOMANYCHANNELS  = '405'
563
+ERR_WASNOSUCHNICK    = '406'
564
+ERR_TOOMANYTARGETS   = '407'
565
+ERR_NOSUCHSERVICE    = '408'
566
+ERR_NOORIGIN         = '409'
567
+ERR_NORECIPIENT      = '411'
568
+ERR_NOTEXTTOSEND     = '412'
569
+ERR_NOTOPLEVEL       = '413'
570
+ERR_WILDTOPLEVEL     = '414'
571
+ERR_BADMASK          = '415'
572
+ERR_UNKNOWNCOMMAND   = '421'
573
+ERR_NOMOTD           = '422'
574
+ERR_NOADMININFO      = '423'
575
+ERR_FILEERROR        = '424'
576
+ERR_NONICKNAMEGIVEN  = '431'
577
+ERR_ERRONEUSNICKNAME = '432'
578
+ERR_NICKNAMEINUSE    = '433'
579
+ERR_NICKCOLLISION    = '436'
580
+ERR_USERNOTINCHANNEL = '441'
581
+ERR_NOTONCHANNEL     = '442'
582
+ERR_USERONCHANNEL    = '443'
583
+ERR_NOLOGIN          = '444'
584
+ERR_SUMMONDISABLED   = '445'
585
+ERR_USERSDISABLED    = '446'
586
+ERR_NOTREGISTERED    = '451'
587
+ERR_NEEDMOREPARAMS   = '461'
588
+ERR_ALREADYREGISTRED = '462'
589
+ERR_NOPERMFORHOST    = '463'
590
+ERR_PASSWDMISMATCH   = '464'
591
+ERR_YOUREBANNEDCREEP = '465'
592
+ERR_KEYSET           = '467'
593
+ERR_CHANNELISFULL    = '471'
594
+ERR_UNKNOWNMODE      = '472'
595
+ERR_INVITEONLYCHAN   = '473'
596
+ERR_BANNEDFROMCHAN   = '474'
597
+ERR_BADCHANNELKEY    = '475'
598
+ERR_BADCHANMASK      = '476'
599
+ERR_BANLISTFULL      = '478'
600
+ERR_NOPRIVILEGES     = '481'
601
+ERR_CHANOPRIVSNEEDED = '482'
602
+ERR_CANTKILLSERVER   = '483'
603
+ERR_UNIQOPRIVSNEEDED = '485'
604
+ERR_NOOPERHOST       = '491'
605
+ERR_UMODEUNKNOWNFLAG = '501'
606
+ERR_USERSDONTMATCH   = '502'
607
+RPL_STARTTLS         = '670'
608
+ERR_STARTTLS         = '691'
609
+RPL_MONONLINE        = '730'
610
+RPL_MONOFFLINE       = '731'
611
+RPL_MONLIST          = '732'
612
+RPL_ENDOFMONLIST     = '733'
613
+ERR_MONLISTFULL      = '734'
614
+RPL_LOGGEDIN         = '900'
615
+RPL_LOGGEDOUT        = '901'
616
+ERR_NICKLOCKED       = '902'
617
+RPL_SASLSUCCESS      = '903'
618
+ERR_SASLFAIL         = '904'
619
+ERR_SASLTOOLONG      = '905'
620
+ERR_SASLABORTED      = '906'
621
+ERR_SASLALREADY      = '907'
622
+RPL_SASLMECHS        = '908'
623
diff --git a/skeleton/core/database.py b/skeleton/core/database.py
624
new file mode 100644
625
index 0000000..2d847eb
626
--- /dev/null
627
+++ b/skeleton/core/database.py
628
@@ -0,0 +1,35 @@
629
+#!/usr/bin/env python
630
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
631
+# database.py
632
+
633
+import os
634
+import re
635
+import sqlite3
636
+
637
+# Globals
638
+db  = sqlite3.connect(os.path.join('data', 'bot.db'), check_same_thread=False)
639
+sql = db.cursor()
640
+
641
+def check():
642
+	tables = sql.execute('SELECT name FROM sqlite_master WHERE type=\'table\'').fetchall()
643
+	if not len(tables):
644
+		sql.execute('CREATE TABLE IGNORE (IDENT TEXT NOT NULL);')
645
+		db.commit()
646
+
647
+class Ignore:
648
+	def add(ident):
649
+		sql.execute('INSERT INTO IGNORE (IDENT) VALUES (?)', (ident,))
650
+		db.commit()
651
+
652
+	def check(ident):
653
+		for ignored_ident in Ignore.read():
654
+			if re.compile(ignored_ident.replace('*','.*')).search(ident):
655
+				return True
656
+		return False
657
+
658
+	def read():
659
+		return list(item[0] for item in sql.execute('SELECT IDENT FROM IGNORE ORDER BY IDENT ASC').fetchall())
660
+
661
+	def remove(ident):
662
+		sql.execute('DELETE FROM IGNORE WHERE IDENT=?', (ident,))
663
+		db.commit()
664
diff --git a/skeleton/core/debug.py b/skeleton/core/debug.py
665
new file mode 100644
666
index 0000000..cf48b24
667
--- /dev/null
668
+++ b/skeleton/core/debug.py
669
@@ -0,0 +1,81 @@
670
+#!/usr/bin/env python
671
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
672
+# debug.py
673
+
674
+import ctypes
675
+import logging
676
+import os
677
+import sys
678
+import time
679
+
680
+from logging.handlers import RotatingFileHandler
681
+
682
+import config
683
+
684
+def check_libs():
685
+	if config.connection.proxy:
686
+		try:
687
+			import socks
688
+		except ImportError:
689
+			error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)')
690
+
691
+def check_privileges():
692
+	if check_windows():
693
+		if ctypes.windll.shell32.IsUserAnAdmin() != 0:
694
+			return True
695
+		else:
696
+			return False
697
+	else:
698
+		if os.getuid() == 0 or os.geteuid() == 0:
699
+			return True
700
+		else:
701
+			return False
702
+
703
+def check_version(major):
704
+	if sys.version_info.major == major:
705
+		return True
706
+	else:
707
+		return False
708
+
709
+def check_windows():
710
+	if os.name == 'nt':
711
+		return True
712
+	else:
713
+		return False
714
+
715
+def clear():
716
+	if check_windows():
717
+		os.system('cls')
718
+	else:
719
+		os.system('clear')
720
+
721
+def error(msg, reason=None):
722
+	if reason:
723
+		logging.debug(f'[!] - {msg} ({reason})')
724
+	else:
725
+		logging.debug('[!] - ' + msg)
726
+
727
+def error_exit(msg):
728
+	raise SystemExit('[!] - ' + msg)
729
+
730
+def info():
731
+	clear()
732
+	logging.debug('#'*56)
733
+	logging.debug('#{0}#'.format(''.center(54)))
734
+	logging.debug('#{0}#'.format('IRC Bot Skeleton'.center(54)))
735
+	logging.debug('#{0}#'.format('Developed by acidvegas in Python'.center(54)))
736
+	logging.debug('#{0}#'.format('https://git.acid.vegas/skeleton'.center(54)))
737
+	logging.debug('#{0}#'.format(''.center(54)))
738
+	logging.debug('#'*56)
739
+
740
+def irc(msg):
741
+	logging.debug('[~] - ' + msg)
742
+
743
+def setup_logger():
744
+	stream_handler = logging.StreamHandler(sys.stdout)
745
+	if config.settings.log:
746
+		log_file     = os.path.join(os.path.join('data','logs'), 'bot.log')
747
+		file_handler = RotatingFileHandler(log_file, maxBytes=256000, backupCount=3)
748
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(file_handler,stream_handler))
749
+	else:
750
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(stream_handler,))
751
diff --git a/skeleton/core/functions.py b/skeleton/core/functions.py
752
new file mode 100644
753
index 0000000..020acb9
754
--- /dev/null
755
+++ b/skeleton/core/functions.py
756
@@ -0,0 +1,10 @@
757
+#!/usr/bin/env python
758
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
759
+# functions.py
760
+
761
+import re
762
+
763
+import config
764
+
765
+def is_admin(ident):
766
+	return re.compile(config.settings.admin.replace('*','.*')).search(ident)
767
diff --git a/skeleton/core/irc.py b/skeleton/core/irc.py
768
new file mode 100644
769
index 0000000..223310d
770
--- /dev/null
771
+++ b/skeleton/core/irc.py
772
@@ -0,0 +1,283 @@
773
+#!/usr/bin/env python
774
+# IRC Bot Skeleton - Developed by acidvegas in Python (https://acid.vegas/skeleton)
775
+# irc.py
776
+
777
+import socket
778
+import time
779
+
780
+import config
781
+import constants
782
+import database
783
+import debug
784
+import functions
785
+
786
+# Load optional modules
787
+if config.connection.ssl:
788
+	import ssl
789
+if config.connection.proxy:
790
+	try:
791
+		import sock
792
+	except ImportError:
793
+		debug.error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') # Required for proxy support.
794
+
795
+def color(msg, foreground, background=None):
796
+	if background:
797
+		return f'\x03{foreground},{background}{msg}{constants.reset}'
798
+	else:
799
+		return f'\x03{foreground}{msg}{constants.reset}'
800
+
801
+class IRC(object):
802
+	def __init__(self):
803
+		self.last   = 0
804
+		self.slow   = False
805
+		self.sock   = None
806
+		self.status = True
807
+
808
+	def connect(self):
809
+		try:
810
+			self.create_socket()
811
+			self.sock.connect((config.connection.server, config.connection.port))
812
+			self.register()
813
+		except socket.error as ex:
814
+			debug.error('Failed to connect to IRC server.', ex)
815
+			Events.disconnect()
816
+		else:
817
+			self.listen()
818
+
819
+	def create_socket(self):
820
+		family = socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET
821
+		if config.connection.proxy:
822
+			proxy_server, proxy_port = config.connection.proxy.split(':')
823
+			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
824
+			self.sock.setblocking(0)
825
+			self.sock.settimeout(15)
826
+			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
827
+		else:
828
+			self.sock = socket.socket(family, socket.SOCK_STREAM)
829
+		if config.connection.vhost:
830
+			self.sock.bind((config.connection.vhost, 0))
831
+		if config.connection.ssl:
832
+			ctx = ssl.SSLContext()
833
+			if config.cert.file:
834
+				ctx.load_cert_chain(config.cert.file, config.cert.key, config.cert.password)
835
+			if config.connection.ssl_verify:
836
+				ctx.verify_mode = ssl.CERT_REQUIRED
837
+				ctx.load_default_certs()
838
+			else:
839
+				ctx.check_hostname = False
840
+				ctx.verify_mode = ssl.CERT_NONE
841
+			self.sock = ctx.wrap_socket(self.sock)
842
+
843
+	def listen(self):
844
+		while True:
845
+			try:
846
+				data = self.sock.recv(2048).decode('utf-8')
847
+				for line in (line for line in data.split('\r\n') if line):
848
+					debug.irc(line)
849
+					if len(line.split()) >= 2:
850
+						Events.handle(line)
851
+			except (UnicodeDecodeError,UnicodeEncodeError):
852
+				pass
853
+			except Exception as ex:
854
+				debug.error('Unexpected error occured.', ex)
855
+				break
856
+		Events.disconnect()
857
+
858
+	def register(self):
859
+		if config.login.network:
860
+			Commands.raw('PASS ' + config.login.network)
861
+		Commands.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}')
862
+		Commands.nick(config.ident.nickname)
863
+
864
+
865
+
866
+class Commands:
867
+	def action(chan, msg):
868
+		Commands.sendmsg(chan, f'\x01ACTION {msg}\x01')
869
+
870
+	def ctcp(target, data):
871
+		Commands.sendmsg(target, f'\001{data}\001')
872
+
873
+	def error(target, data, reason=None):
874
+		if reason:
875
+			Commands.sendmsg(target, '[{0}] {1} {2}'.format(color('!', constants.red), data, color('({0})'.format(reason), constants.grey)))
876
+		else:
877
+			Commands.sendmsg(target, '[{0}] {1}'.format(color('!', constants.red), data))
878
+
879
+	def identify(nick, password):
880
+		Commands.sendmsg('nickserv', f'identify {nick} {password}')
881
+
882
+	def invite(nick, chan):
883
+		Commands.raw(f'INVITE {nick} {chan}')
884
+
885
+	def join_channel(chan, key=None):
886
+		Commands.raw(f'JOIN {chan} {key}') if msg else Commands.raw('JOIN ' + chan)
887
+
888
+	def mode(target, mode):
889
+		Commands.raw(f'MODE {target} {mode}')
890
+
891
+	def nick(nick):
892
+		Commands.raw('NICK ' + nick)
893
+
894
+	def notice(target, msg):
895
+		Commands.raw(f'NOTICE {target} :{msg}')
896
+
897
+	def oper(user, password):
898
+		Commands.raw(f'OPER {user} {password}')
899
+
900
+	def part(chan, msg=None):
901
+		Commands.raw(f'PART {chan} {msg}') if msg else Commands.raw('PART ' + chan)
902
+
903
+	def quit(msg=None):
904
+		Commands.raw('QUIT :' + msg) if msg else Commands.raw('QUIT')
905
+
906
+	def raw(msg):
907
+		Bot.sock.send(bytes(msg + '\r\n', 'utf-8'))
908
+
909
+	def sendmsg(target, msg):
910
+		Commands.raw(f'PRIVMSG {target} :{msg}')
911
+
912
+	def topic(chan, text):
913
+		Commands.raw(f'TOPIC {chan} :{text}')
914
+
915
+
916
+
917
+class Events:
918
+	def connect():
919
+		if config.settings.modes:
920
+			Commands.mode(config.ident.nickname, '+' + config.settings.modes)
921
+		if config.login.nickserv:
922
+			Commands.identify(config.ident.nickname, config.login.nickserv)
923
+		if config.login.operator:
924
+			Commands.oper(config.ident.username, config.login.operator)
925
+		Commands.join_channel(config.connection.channel, config.connection.key)
926
+
927
+	def ctcp(nick, chan, msg):
928
+		pass
929
+
930
+	def disconnect():
931
+		Bot.sock.close()
932
+		time.sleep(config.throttle.reconnect)
933
+		Bot.connect()
934
+
935
+	def invite(nick, chan):
936
+		if nick == config.ident.nickname and chan == config.connection.channe:
937
+			Commands.join_channel(config.connection.channel, config.connection.key)
938
+
939
+	def join_channel(nick, chan):
940
+		pass
941
+
942
+	def kick(nick, chan, kicked):
943
+		if kicked == config.ident.nickname and chan == config.connection.channel:
944
+			time.sleep(config.throttle.rejoin)
945
+			Commands.join_channel(chan, config.connection.key)
946
+
947
+	def message(nick, ident, chan, msg):
948
+		try:
949
+			if chan == config.connection.channel and Bot.status:
950
+				if msg.startswith(config.settings.cmd_char):
951
+					if not database.Ignore.check(ident):
952
+						if time.time() - Bot.last < config.throttle.command and not functions.is_admin(ident):
953
+							if not Bot.slow:
954
+								Commands.sendmsg(chan, color('Slow down nerd!', constants.red))
955
+								Bot.slow = True
956
+						elif Bot.status or functions.is_admin(ident):
957
+							Bot.slow = False
958
+							args     = msg.split()
959
+							if len(args) == 1:
960
+								if cmd == 'test':
961
+									Commands.sendmsg(chan, 'It works!')
962
+							elif len(args) >= 2:
963
+								if cmd == 'echo':
964
+									Commands.sendmsg(chan, args)
965
+						Bot.last = time.time()
966
+		except Exception as ex:
967
+			Commands.error(chan, 'Command threw an exception.', ex)
968
+
969
+	def nick_in_use():
970
+		debug.error('The bot is already running or nick is in use.')
971
+
972
+	def part(nick, chan):
973
+		pass
974
+
975
+	def private(nick, ident, msg):
976
+		if functions.is_admin(ident):
977
+			args = msg.split()
978
+			if msg == '.ignore':
979
+				ignores = database.Ignore.read()
980
+				if ignores:
981
+					Commands.sendmsg(nick, '[{0}]'.format(color('Ignore List', constants.purple)))
982
+					for user in ignores:
983
+						Commands.sendmsg(nick, color(user, constants.yellow))
984
+					Commands.sendmsg(nick, '{0} {1}'.format(color('Total:', constants.light_blue), color(len(ignores), constants.grey)))
985
+				else:
986
+					Commands.error(nick, 'Ignore list is empty!')
987
+			elif msg == '.off':
988
+				Bot.status = False
989
+				Commands.sendmsg(nick, color('OFF', constants.red))
990
+			elif msg == '.on':
991
+				Bot.status = True
992
+				Commands.sendmsg(nick, color('ON', constants.green))
993
+			elif len(args) == 3:
994
+				if args[0] == '.ignore':
995
+					if args[1] == 'add':
996
+						user_ident = args[2]
997
+						if user_ident not in database.Ignore.hosts():
998
+							database.Ignore.add(nickname, user_ident)
999
+							Commands.sendmsg(nick, 'Ident {0} to the ignore list.'.format(color('added', constants.green)))