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))) |