←︎ scroll :: dfa165f


1
commit dfa165f330dfbc820455dd909a77cd04740809a4
2
Author: acidvegas <acid.vegas@acid.vegas>
3
Date:   Fri Aug 2 01:55:14 2019 -0400
4
5
    Initial commit
6
---
7
 LICENSE                        |  15 ++
8
 README.md                      |  75 +++++++++
9
 screens/preview.png            | Bin 0 -> 134210 bytes
10
 scroll/core/ascii2png.py       | 166 +++++++++++++++++++
11
 scroll/core/config.py          |  36 +++++
12
 scroll/core/constants.py       | 229 ++++++++++++++++++++++++++
13
 scroll/core/database.py        |  74 +++++++++
14
 scroll/core/debug.py           |  56 +++++++
15
 scroll/core/functions.py       |  58 +++++++
16
 scroll/core/irc.py             | 354 +++++++++++++++++++++++++++++++++++++++++
17
 scroll/data/.gitignore         |   1 +
18
 scroll/data/DejaVuSansMono.ttf | Bin 0 -> 340712 bytes
19
 scroll/data/art/.gitignore     |   4 +
20
 scroll/data/logs/.gitignore    |   4 +
21
 scroll/scroll.py               |  23 +++
22
 15 files changed, 1095 insertions(+)
23
24
diff --git a/LICENSE b/LICENSE
25
new file mode 100644
26
index 0000000..b63b809
27
--- /dev/null
28
+++ b/LICENSE
29
@@ -0,0 +1,15 @@
30
+ISC License
31
+
32
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
33
+
34
+Permission to use, copy, modify, and/or distribute this software for any
35
+purpose with or without fee is hereby granted, provided that the above
36
+copyright notice and this permission notice appear in all copies.
37
+
38
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
39
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
40
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
41
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
42
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
43
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
44
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
45
diff --git a/README.md b/README.md
46
new file mode 100644
47
index 0000000..bb4cf7b
48
--- /dev/null
49
+++ b/README.md
50
@@ -0,0 +1,75 @@
51
+# scroll
52
+> Scroll is full-featured IRC bot that carries a **PENIS PUMP** & will brighten up your mundane chats in your lame channels with some colorful IRC artwork! Designed to be extremely stable, this bot is sure to stay rock hard & handle itself quite well!
53
+
54
+![](screens/preview.png)
55
+
56
+## Requirements
57
+- [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python)*
58
+- [Pillow](https://pypi.org/project/Pillow/) *(Required by [core/ascii2png.py](scroll/core/ascii2png.py)*
59
+
60
+## Setup
61
+Edit the [core/config.py](scroll/core/config.py) file and then place your art files in the [data/art](scroll/data/art) directory. This repository by itself does not contain any IRC art files. A large organized collection of IRC art can be cloned from the [ircart](https://github.com/ircart/ircart) repository directory into your [data/art](scroll/data/art) directory by doing `git clone https://github.com/ircart/ircart.git $SCROLL_DIR/scroll/data/art` *(change the $SCROLL_DIR to wherever you cloned the source)*.
62
+
63
+**Warning:** Try not to have any filenames in your [data/art](scroll/data/art) directory that are the same as any of the [Commands](#commands) below or you won't be able to play them!
64
+
65
+## Commands
66
+### User Commands
67
+| Command                         | Description                                                                                                                                             |
68
+| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+| @scroll                         | information about scroll                                                                                                                                |
70
+| .ascii dirs                     | list of ascii directories                                                                                                                               |
71
+| .ascii list                     | list of ascii filenames                                                                                                                                 |
72
+| .ascii png \<url>               | convert an art paste into an image *(\<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link)*                       |
73
+| .ascii random [dir]             | play random art, optionally from the [dir] directory only                                                                                               |
74
+| .ascii remote \<url>            | play remote art pastes *(\<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link)*                                   |
75
+| .ascii search \<query>          | search [data/art](scroll/data/art) for \<query>                                                                                                         |
76
+| .ascii stop                     | stop playing art                                                                                                                                        |
77
+| .ascii upload [\<url> \<title>] | list of uploaded art pastes or upload \<url> as \<title> *(\<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link)* |
78
+| .ascii \<name> [\<trunc>]       | play \<name> art from [data/art](scroll/data/art) *(see usage below)*                                                                                   |
79
+
80
+#### Note
81
+The \<trunc> argument for the `.ascii` command allows you to truncate lines off of an ASCII. The data is TOP,BOTTOM,LEFT,RIGHT,SPACES. Top being how many lines to remove from the top. Bottom being the same except from the bottom. Left and right remove characters from each side, and spaces will prefix lines with this many spaces.
82
+
83
+**Example:** `.ascii funnyguy 3,5,0,10,30` *(This will remove 3 lines from the top, 5 lines from the bottom, no characters from the left, 10 characters from the right, and append 30 spaces before each line)*
84
+
85
+### Admin Commands *(Private Message)*
86
+| Command                       | Description                                                                                                  |
87
+| ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
88
+| .config [\<setting> \<value>] | view config or change \<setting> to \<value>                                                                 |
89
+| .ignore [\<add/del> \<ident>] | view ignore list or \<add/del> an \<ident> *(\<ident> must be in nick!user@host format, wildcards accepted)* |
90
+| .raw \<data>                  | send \<data> to the server                                                                                   |
91
+| .update                       | update the [data/art](scroll/data/art) directory *(see usage below)*                                         |
92
+
93
+#### Note
94
+The `.update` command is if you cloned a git repository, like the one mentioned in the [Setup](#setup) section, for your [data/ascii](scroll/data/ascii) directory. This will simply perform a `git pull` on the repository to update it.
95
+
96
+## Config Settings
97
+| Setting         | Description                                                                           |
98
+| --------------- | ------------------------------------------------------------------------------------- |
99
+| max_lines       | maximum number of lines an art can be to be played outside of the **#scroll** channel |
100
+| max_png_bytes   | maximum size *(in bytes)* for `.ascii png` usage                                      |
101
+| max_results     | maximum number of results returned from a search                                      |
102
+| max_uploads     | maximum number of uploads to store                                                    |
103
+| max_uploads_per | maximum number of uploads per-nick                                                    |
104
+| throttle_cmd    | command usage throttle in seconds                                                     |
105
+| throttle_msg    | message throttle in seconds                                                           |
106
+| throttle_png    | `.ascii png` throttle in seconds                                                      |
107
+| rnd_exclude     | directories to ignore with `.ascii random`. *(comma-seperated list)*                  |
108
+
109
+## Screens
110
+This section is not finished. Screens will be added soon.
111
+
112
+## Todo
113
+* Check paste size before downloading.
114
+* Store image bytes in memory so creating files locally is not required.
115
+* Improve truncation of the left and right by retaining the last known color code.
116
+* Add ascii tagging to database. Lets you tag asciis with hashtags for searching. Maybe include an author tag also.
117
+* Add an ascii amplification factor and zigzag to the trunc array, and maybe rename trunc to morph.
118
+* Use the maximum allowed upload size for IMGUR upload, and track it to a maximum of 50 per hour. 
119
+* Controls for admins to move, rename, delete, and download ascii on the fly.
120
+
121
+## Mirrors
122
+- [acid.vegas](https://acid.vegas/scroll) *(main)*
123
+- [SuperNETs](https://git.supernets.org/ircart/scroll)
124
+- [GitHub](https://github.com/ircart/scroll)
125
+- [GitLab](https://gitlab.com/ircart/scroll)
126
diff --git a/screens/preview.png b/screens/preview.png
127
new file mode 100644
128
index 0000000..3f8f8e3
129
Binary files /dev/null and b/screens/preview.png differ
130
diff --git a/scroll/core/ascii2png.py b/scroll/core/ascii2png.py
131
new file mode 100644
132
index 0000000..1194d61
133
--- /dev/null
134
+++ b/scroll/core/ascii2png.py
135
@@ -0,0 +1,166 @@
136
+#!/usr/bin/env python
137
+# -*- coding: utf-8 -*-
138
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
139
+# ascii2png.py
140
+
141
+'''
142
+Credits to VXP for making the original "pngbot" script (https://github.com/lalbornoz/MiRCARTools)
143
+'''
144
+
145
+import os
146
+import urllib.request
147
+
148
+from PIL import Image, ImageDraw, ImageFont
149
+
150
+def flip_cell_state(cellState, bit):
151
+	if cellState & bit:
152
+		return cellState & ~bit
153
+	else:
154
+		return cellState | bit
155
+
156
+def parse_char(colourSpec, curColours):
157
+	if len(colourSpec) > 0:
158
+		colourSpec = colourSpec.split(',')
159
+		if len(colourSpec) == 2 and len(colourSpec[1]) > 0:
160
+			return (int(colourSpec[0] or curColours[0]), int(colourSpec[1]))
161
+		elif len(colourSpec) == 1 or len(colourSpec[1]) == 0:
162
+			return (int(colourSpec[0]), curColours[1])
163
+	else:
164
+		return (15, 1)
165
+
166
+def ascii_png(url):
167
+	text_file = os.path.join('data','temp.txt')
168
+	if os.path.isfile(text_file):
169
+		os.remove(text_file)
170
+	urllib.request.urlretrieve(url, text_file)
171
+	data = open(text_file)
172
+	inCurColourSpec = ''
173
+	inCurRow = -1
174
+	inLine = data.readline()
175
+	inSize = [0, 0]
176
+	inMaxCols = 0
177
+	outMap = []
178
+	while inLine:
179
+		inCellState = 0x00
180
+		inParseState = 1
181
+		inCurCol = 0
182
+		inMaxCol = len(inLine)
183
+		inCurColourDigits = 0
184
+		inCurColours = (15, 1)
185
+		inCurColourSpec = ''
186
+		inCurRow += 1
187
+		outMap.append([])
188
+		inRowCols = 0
189
+		inSize[1] += 1
190
+		while inCurCol < inMaxCol:
191
+			inChar = inLine[inCurCol]
192
+			if inChar in set('\r\n'):
193
+				inCurCol += 1
194
+			elif inParseState == 1:
195
+				inCurCol += 1
196
+				if inChar == '':
197
+					inCellState = flip_cell_state(inCellState, 0x01)
198
+				elif inChar == '':
199
+					inParseState = 2
200
+				elif inChar == '':
201
+					inCellState = flip_cell_state(inCellState, 0x02)
202
+				elif inChar == '':
203
+					inCellState |= 0x00
204
+					inCurColours = (15, 1)
205
+				elif inChar == '':
206
+					inCurColours = (inCurColours[1], inCurColours[0])
207
+				elif inChar == '':
208
+					inCellState = flip_cell_state(inCellState, 0x04)
209
+				else:
210
+					inRowCols += 1
211
+					outMap[inCurRow].append([*inCurColours, inCellState, inChar])
212
+			elif inParseState == 2 or inParseState == 3:
213
+				if inChar == ',' and inParseState == 2:
214
+					if (inCurCol + 1) < inMaxCol and not inLine[inCurCol + 1] in set('0123456789'):
215
+						inCurColours = parse_char(inCurColourSpec, inCurColours)
216
+						inCurColourDigits = 0
217
+						inCurColourSpec = ''
218
+						inParseState = 1
219
+					else:
220
+						inCurCol += 1
221
+						inCurColourDigits = 0
222
+						inCurColourSpec += inChar
223
+						inParseState = 3
224
+				elif inChar in set('0123456789') and inCurColourDigits == 0:
225
+					inCurCol += 1
226
+					inCurColourDigits += 1
227
+					inCurColourSpec += inChar
228
+				elif inChar in set('0123456789') and inCurColourDigits == 1 and inCurColourSpec[-1] == '0':
229
+					inCurCol += 1
230
+					inCurColourDigits += 1
231
+					inCurColourSpec += inChar
232
+				elif inChar in set('012345') and inCurColourDigits == 1 and inCurColourSpec[-1] == '1':
233
+					inCurCol += 1
234
+					inCurColourDigits += 1
235
+					inCurColourSpec += inChar
236
+				else:
237
+					inCurColours = parse_char(inCurColourSpec, inCurColours)
238
+					inCurColourDigits = 0
239
+					inCurColourSpec = ''
240
+					inParseState = 1
241
+		inMaxCols = max(inMaxCols, inRowCols)
242
+		inLine = data.readline()
243
+	inSize[0] = inMaxCols
244
+	canvas_data = outMap
245
+	numRowCols = 0
246
+	for numRow in range(len(outMap)):
247
+		numRowCols = max(numRowCols, len(outMap[numRow]))
248
+	for numRow in range(len(outMap)):
249
+		if len(outMap[numRow]) != numRowCols:
250
+			for numColOff in range(numRowCols - len(outMap[numRow])):
251
+				outMap[numRow].append([1,1,0,' '])
252
+		outMap[numRow].insert(0,[1,1,0,' '])
253
+		outMap[numRow].append([1,1,0,' '])
254
+	outMap.insert(0,[[1,1,0,' ']] * len(outMap[0]))
255
+	outMap.append([[1,1,0,' ']] * len(outMap[0]))
256
+	inCanvasMap = outMap
257
+	outImgFont = ImageFont.truetype(os.path.join('data','DejaVuSansMono.ttf'), 11)
258
+	outImgFontSize = [*outImgFont.getsize(' ')]
259
+	outImgFontSize[1] += 3
260
+	ColorsBold   = [[255,255,255],[85,85,85],[85,85,255],[85,255,85],[255,85,85],[255,85,85],[255,85,255],[255,255,85],[255,255,85],[85,255,85],[85,255,255],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[255,255,255]]
261
+	ColorsNormal = [[255,255,255],[0,0,0],[0,0,187],[0,187,0],[255,85,85],[187,0,0],[187,0,187],[187,187,0],[255,255,85],[85,255,85],[0,187,187],[85,255,255],[85,85,255],[255,85,255],[85,85,85],[187,187,187]]
262
+	inSize = (len(inCanvasMap[0]), len(inCanvasMap))
263
+	outSize = [a*b for a,b in zip(inSize, outImgFontSize)]
264
+	outCurPos = [0, 0]
265
+	outImg = Image.new('RGBA', outSize, (*ColorsNormal[1], 255))
266
+	outImgDraw = ImageDraw.Draw(outImg)
267
+	for inCurRow in range(len(inCanvasMap)):
268
+		for inCurCol in range(len(inCanvasMap[inCurRow])):
269
+			inCurCell = inCanvasMap[inCurRow][inCurCol]
270
+			outColours = [0, 0]
271
+			if inCurCell[2] & 0x01:
272
+				if inCurCell[3] != ' ':
273
+					if inCurCell[3] == '█':
274
+						outColours[1] = ColorsNormal[inCurCell[0]]
275
+					else:
276
+						outColours[0] = ColorsBold[inCurCell[0]]
277
+						outColours[1] = ColorsNormal[inCurCell[1]]
278
+				else:
279
+					outColours[1] = ColorsNormal[inCurCell[1]]
280
+			else:
281
+				if inCurCell[3] != ' ':
282
+					if inCurCell[3] == '█':
283
+						outColours[1] = ColorsNormal[inCurCell[0]]
284
+					else:
285
+						outColours[0] = ColorsNormal[inCurCell[0]]
286
+						outColours[1] = ColorsNormal[inCurCell[1]]
287
+				else:
288
+					outColours[1] = ColorsNormal[inCurCell[1]]
289
+			outImgDraw.rectangle((*outCurPos,outCurPos[0] + outImgFontSize[0], outCurPos[1] + outImgFontSize[1]), fill=(*outColours[1], 255))
290
+			if  not inCurCell[3] in ' █' and outColours[0] != outColours[1]:
291
+				outImgDraw.text(outCurPos,inCurCell[3], (*outColours[0], 255), outImgFont)
292
+			if inCurCell[2] & 0x04:
293
+				outColours[0] = ColorsNormal[inCurCell[0]]
294
+				outImgDraw.line(xy=(outCurPos[0], outCurPos[1] + (outImgFontSize[1] - 2), outCurPos[0] + outImgFontSize[0], outCurPos[1] + (outImgFontSize[1] - 2)), fill=(*outColours[0], 255))
295
+			outCurPos[0] += outImgFontSize[0]
296
+		outCurPos[0] = 0
297
+		outCurPos[1] += outImgFontSize[1]
298
+	out_file = os.path.join('data','temp.png')
299
+	if os.path.isfile(out_file):
300
+		os.remove(out_file)
301
+	outImg.save(out_file)
302
diff --git a/scroll/core/config.py b/scroll/core/config.py
303
new file mode 100644
304
index 0000000..fecab55
305
--- /dev/null
306
+++ b/scroll/core/config.py
307
@@ -0,0 +1,36 @@
308
+#!/usr/bin/env python
309
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
310
+# config.py
311
+
312
+class connection:
313
+	server     = 'irc.server.com'
314
+	port       = 6697
315
+	proxy      = None
316
+	ipv6       = False
317
+	ssl        = True
318
+	ssl_verify = False
319
+	vhost      = None
320
+	channel    = '#chats'
321
+	key        = None
322
+
323
+class cert:
324
+	key      = None
325
+	file     = None
326
+	password = None
327
+
328
+class ident:
329
+	nickname = 'scroll'
330
+	username = 'scroll'
331
+	realname = 'acid.vegas/scroll'
332
+
333
+class login:
334
+	network  = None
335
+	nickserv = None
336
+	operator = None
337
+
338
+class settings:
339
+	admin = 'nick!user@host' # Must be in nick!user@host format (Can use wildcards here)
340
+	log   = False
341
+	modes = None
342
+
343
+IMGUR_API_KEY = 'CHANGEME' # https://apidocs.imgur.com/
344
diff --git a/scroll/core/constants.py b/scroll/core/constants.py
345
new file mode 100644
346
index 0000000..03d3d52
347
--- /dev/null
348
+++ b/scroll/core/constants.py
349
@@ -0,0 +1,229 @@
350
+#!/usr/bin/env python
351
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
352
+# constants.py
353
+
354
+# Control Characters
355
+bold      = '\x02'
356
+color     = '\x03'
357
+italic    = '\x1D'
358
+underline = '\x1F'
359
+reverse   = '\x16'
360
+reset     = '\x0f'
361
+
362
+# Color Codes
363
+white       = '00'
364
+black       = '01'
365
+blue        = '02'
366
+green       = '03'
367
+red         = '04'
368
+brown       = '05'
369
+purple      = '06'
370
+orange      = '07'
371
+yellow      = '08'
372
+light_green = '09'
373
+cyan        = '10'
374
+light_cyan  = '11'
375
+light_blue  = '12'
376
+pink        = '13'
377
+grey        = '14'
378
+light_grey  = '15'
379
+
380
+# Events
381
+PASS     = 'PASS'
382
+NICK     = 'NICK'
383
+USER     = 'USER'
384
+OPER     = 'OPER'
385
+MODE     = 'MODE'
386
+SERVICE  = 'SERVICE'
387
+QUIT     = 'QUIT'
388
+SQUIT    = 'SQUIT'
389
+JOIN     = 'JOIN'
390
+PART     = 'PART'
391
+TOPIC    = 'TOPIC'
392
+NAMES    = 'NAMES'
393
+LIST     = 'LIST'
394
+INVITE   = 'INVITE'
395
+KICK     = 'KICK'
396
+PRIVMSG  = 'PRIVMSG'
397
+NOTICE   = 'NOTICE'
398
+MOTD     = 'MOTD'
399
+LUSERS   = 'LUSERS'
400
+VERSION  = 'VERSION'
401
+STATS    = 'STATS'
402
+LINKS    = 'LINKS'
403
+TIME     = 'TIME'
404
+CONNECT  = 'CONNECT'
405
+TRACE    = 'TRACE'
406
+ADMIN    = 'ADMIN'
407
+INFO     = 'INFO'
408
+SERVLIST = 'SERVLIST'
409
+SQUERY   = 'SQUERY'
410
+WHO      = 'WHO'
411
+WHOIS    = 'WHOIS'
412
+WHOWAS   = 'WHOWAS'
413
+KILL     = 'KILL'
414
+PING     = 'PING'
415
+PONG     = 'PONG'
416
+ERROR    = 'ERROR'
417
+AWAY     = 'AWAY'
418
+REHASH   = 'REHASH'
419
+DIE      = 'DIE'
420
+RESTART  = 'RESTART'
421
+SUMMON   = 'SUMMON'
422
+USERS    = 'USERS'
423
+WALLOPS  = 'WALLOPS'
424
+USERHOST = 'USERHOST'
425
+ISON     = 'ISON'
426
+
427
+# Event Numerics
428
+RPL_WELCOME          = '001'
429
+RPL_YOURHOST         = '002'
430
+RPL_CREATED          = '003'
431
+RPL_MYINFO           = '004'
432
+RPL_ISUPPORT         = '005'
433
+RPL_TRACELINK        = '200'
434
+RPL_TRACECONNECTING  = '201'
435
+RPL_TRACEHANDSHAKE   = '202'
436
+RPL_TRACEUNKNOWN     = '203'
437
+RPL_TRACEOPERATOR    = '204'
438
+RPL_TRACEUSER        = '205'
439
+RPL_TRACESERVER      = '206'
440
+RPL_TRACESERVICE     = '207'
441
+RPL_TRACENEWTYPE     = '208'
442
+RPL_TRACECLASS       = '209'
443
+RPL_STATSLINKINFO    = '211'
444
+RPL_STATSCOMMANDS    = '212'
445
+RPL_STATSCLINE       = '213'
446
+RPL_STATSILINE       = '215'
447
+RPL_STATSKLINE       = '216'
448
+RPL_STATSYLINE       = '218'
449
+RPL_ENDOFSTATS       = '219'
450
+RPL_UMODEIS          = '221'
451
+RPL_SERVLIST         = '234'
452
+RPL_SERVLISTEND      = '235'
453
+RPL_STATSLLINE       = '241'
454
+RPL_STATSUPTIME      = '242'
455
+RPL_STATSOLINE       = '243'
456
+RPL_STATSHLINE       = '244'
457
+RPL_LUSERCLIENT      = '251'
458
+RPL_LUSEROP          = '252'
459
+RPL_LUSERUNKNOWN     = '253'
460
+RPL_LUSERCHANNELS    = '254'
461
+RPL_LUSERME          = '255'
462
+RPL_ADMINME          = '256'
463
+RPL_ADMINLOC1        = '257'
464
+RPL_ADMINLOC2        = '258'
465
+RPL_ADMINEMAIL       = '259'
466
+RPL_TRACELOG         = '261'
467
+RPL_TRYAGAIN         = '263'
468
+RPL_NONE             = '300'
469
+RPL_AWAY             = '301'
470
+RPL_USERHOST         = '302'
471
+RPL_ISON             = '303'
472
+RPL_UNAWAY           = '305'
473
+RPL_NOWAWAY          = '306'
474
+RPL_WHOISUSER        = '311'
475
+RPL_WHOISSERVER      = '312'
476
+RPL_WHOISOPERATOR    = '313'
477
+RPL_WHOWASUSER       = '314'
478
+RPL_ENDOFWHO         = '315'
479
+RPL_WHOISIDLE        = '317'
480
+RPL_ENDOFWHOIS       = '318'
481
+RPL_WHOISCHANNELS    = '319'
482
+RPL_LIST             = '322'
483
+RPL_LISTEND          = '323'
484
+RPL_CHANNELMODEIS    = '324'
485
+RPL_NOTOPIC          = '331'
486
+RPL_TOPIC            = '332'
487
+RPL_INVITING         = '341'
488
+RPL_INVITELIST       = '346'
489
+RPL_ENDOFINVITELIST  = '347'
490
+RPL_EXCEPTLIST       = '348'
491
+RPL_ENDOFEXCEPTLIST  = '349'
492
+RPL_VERSION          = '351'
493
+RPL_WHOREPLY         = '352'
494
+RPL_NAMREPLY         = '353'
495
+RPL_LINKS            = '364'
496
+RPL_ENDOFLINKS       = '365'
497
+RPL_ENDOFNAMES       = '366'
498
+RPL_BANLIST          = '367'
499
+RPL_ENDOFBANLIST     = '368'
500
+RPL_ENDOFWHOWAS      = '369'
501
+RPL_INFO             = '371'
502
+RPL_MOTD             = '372'
503
+RPL_ENDOFINFO        = '374'
504
+RPL_MOTDSTART        = '375'
505
+RPL_ENDOFMOTD        = '376'
506
+RPL_YOUREOPER        = '381'
507
+RPL_REHASHING        = '382'
508
+RPL_YOURESERVICE     = '383'
509
+RPL_TIME             = '391'
510
+RPL_USERSSTART       = '392'
511
+RPL_USERS            = '393'
512
+RPL_ENDOFUSERS       = '394'
513
+RPL_NOUSERS          = '395'
514
+ERR_NOSUCHNICK       = '401'
515
+ERR_NOSUCHSERVER     = '402'
516
+ERR_NOSUCHCHANNEL    = '403'
517
+ERR_CANNOTSENDTOCHAN = '404'
518
+ERR_TOOMANYCHANNELS  = '405'
519
+ERR_WASNOSUCHNICK    = '406'
520
+ERR_TOOMANYTARGETS   = '407'
521
+ERR_NOSUCHSERVICE    = '408'
522
+ERR_NOORIGIN         = '409'
523
+ERR_NORECIPIENT      = '411'
524
+ERR_NOTEXTTOSEND     = '412'
525
+ERR_NOTOPLEVEL       = '413'
526
+ERR_WILDTOPLEVEL     = '414'
527
+ERR_BADMASK          = '415'
528
+ERR_UNKNOWNCOMMAND   = '421'
529
+ERR_NOMOTD           = '422'
530
+ERR_NOADMININFO      = '423'
531
+ERR_FILEERROR        = '424'
532
+ERR_NONICKNAMEGIVEN  = '431'
533
+ERR_ERRONEUSNICKNAME = '432'
534
+ERR_NICKNAMEINUSE    = '433'
535
+ERR_NICKCOLLISION    = '436'
536
+ERR_USERNOTINCHANNEL = '441'
537
+ERR_NOTONCHANNEL     = '442'
538
+ERR_USERONCHANNEL    = '443'
539
+ERR_NOLOGIN          = '444'
540
+ERR_SUMMONDISABLED   = '445'
541
+ERR_USERSDISABLED    = '446'
542
+ERR_NOTREGISTERED    = '451'
543
+ERR_NEEDMOREPARAMS   = '461'
544
+ERR_ALREADYREGISTRED = '462'
545
+ERR_NOPERMFORHOST    = '463'
546
+ERR_PASSWDMISMATCH   = '464'
547
+ERR_YOUREBANNEDCREEP = '465'
548
+ERR_KEYSET           = '467'
549
+ERR_CHANNELISFULL    = '471'
550
+ERR_UNKNOWNMODE      = '472'
551
+ERR_INVITEONLYCHAN   = '473'
552
+ERR_BANNEDFROMCHAN   = '474'
553
+ERR_BADCHANNELKEY    = '475'
554
+ERR_BADCHANMASK      = '476'
555
+ERR_BANLISTFULL      = '478'
556
+ERR_NOPRIVILEGES     = '481'
557
+ERR_CHANOPRIVSNEEDED = '482'
558
+ERR_CANTKILLSERVER   = '483'
559
+ERR_UNIQOPRIVSNEEDED = '485'
560
+ERR_NOOPERHOST       = '491'
561
+ERR_UMODEUNKNOWNFLAG = '501'
562
+ERR_USERSDONTMATCH   = '502'
563
+RPL_STARTTLS         = '670'
564
+ERR_STARTTLS         = '691'
565
+RPL_MONONLINE        = '730'
566
+RPL_MONOFFLINE       = '731'
567
+RPL_MONLIST          = '732'
568
+RPL_ENDOFMONLIST     = '733'
569
+ERR_MONLISTFULL      = '734'
570
+RPL_LOGGEDIN         = '900'
571
+RPL_LOGGEDOUT        = '901'
572
+ERR_NICKLOCKED       = '902'
573
+RPL_SASLSUCCESS      = '903'
574
+ERR_SASLFAIL         = '904'
575
+ERR_SASLTOOLONG      = '905'
576
+ERR_SASLABORTED      = '906'
577
+ERR_SASLALREADY      = '907'
578
+RPL_SASLMECHS        = '908'
579
diff --git a/scroll/core/database.py b/scroll/core/database.py
580
new file mode 100644
581
index 0000000..d4b8f8d
582
--- /dev/null
583
+++ b/scroll/core/database.py
584
@@ -0,0 +1,74 @@
585
+#!/usr/bin/env python
586
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
587
+# database.py
588
+
589
+import os
590
+import re
591
+import sqlite3
592
+
593
+db  = sqlite3.connect(os.path.join('data', 'scroll.db'), check_same_thread=False)
594
+sql = db.cursor()
595
+
596
+def check():
597
+	tables = sql.execute('SELECT name FROM sqlite_master WHERE type=\'table\'').fetchall()
598
+	if not len(tables):
599
+		sql.execute('CREATE TABLE IGNORE (IDENT TEXT NOT NULL);')
600
+		sql.execute('CREATE TABLE SETTINGS (SETTING TEXT NOT NULL, VALUE TEXT NOT NULL);')
601
+		sql.execute('CREATE TABLE UPLOADS (NICK TEXT NOT NULL, URL TEXT NOT NULL, TITLE TEXT NOT NULL);')
602
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_lines',       '300'))
603
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_png_bytes',   '2000000'))
604
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_results',     '10'))
605
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads',     '25'))
606
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads_per', '5'))
607
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_cmd',    '3'))
608
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_msg',    '0.03'))
609
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('throttle_png',    '0.03'))
610
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('rnd_exclude',     'ansi,big,birds,hang,pokemon'))
611
+		db.commit()
612
+
613
+class Ignore:
614
+	def add(ident):
615
+		sql.execute('INSERT INTO IGNORE (IDENT) VALUES (?)', (ident,))
616
+		db.commit()
617
+
618
+	def check(ident):
619
+		for ignored_ident in Ignore.read():
620
+			if re.compile(ignored_ident.replace('*','.*')).search(ident):
621
+				return True
622
+		return False
623
+
624
+	def read():
625
+		return [item[0] for item in sql.execute('SELECT IDENT FROM IGNORE').fetchall()]
626
+
627
+	def remove(ident):
628
+		sql.execute('DELETE FROM IGNORE WHERE IDENT=?', (ident,))
629
+		db.commit()
630
+
631
+class Settings:
632
+	def get(setting):
633
+		return sql.execute('SELECT VALUE FROM SETTINGS WHERE SETTING=?', (setting,)).fetchone()[0]
634
+
635
+	def read():
636
+		return sql.execute('SELECT SETTING,VALUE FROM SETTINGS ORDER BY SETTING ASC').fetchall()
637
+
638
+	def settings():
639
+		return list(item[0] for item in sql.execute('SELECT SETTING FROM SETTINGS').fetchall())
640
+
641
+	def update(setting, value):
642
+		sql.execute('UPDATE SETTINGS SET VALUE=? WHERE SETTING=?', (value, setting))
643
+		db.commit()
644
+
645
+class Uploads:
646
+	def add(nick, url, title):
647
+		sql.execute('INSERT INTO UPLOADS (NICK,URL,TITLE) VALUES (?,?,?)', (nick,url,title))
648
+		db.commit()
649
+
650
+	def read(nick=None):
651
+		if nick:
652
+			return sql.execute('SELECT NICK,URL,TITLE FROM UPLOADS WHERE NICK=?', (nick,)).fetchall()
653
+		else:
654
+			return sql.execute('SELECT NICK,URL,TITLE FROM UPLOADS').fetchall()
655
+
656
+	def remove(url):
657
+		sql.execute('DELETE FROM UPLOADS WHERE URL=?', (url,))
658
+		db.commit()
659
diff --git a/scroll/core/debug.py b/scroll/core/debug.py
660
new file mode 100644
661
index 0000000..5c29f15
662
--- /dev/null
663
+++ b/scroll/core/debug.py
664
@@ -0,0 +1,56 @@
665
+#!/usr/bin/env python
666
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
667
+# debug.py
668
+
669
+import ctypes
670
+import logging
671
+import os
672
+import sys
673
+import time
674
+
675
+from logging.handlers import RotatingFileHandler
676
+
677
+import config
678
+
679
+def check_privileges():
680
+	if check_windows():
681
+		return True if ctypes.windll.shell32.IsUserAnAdmin() else False
682
+	else:
683
+		return True if os.getuid() == 0 or os.geteuid() == 0 else False
684
+
685
+def check_version(major):
686
+	return True if sys.version_info.major == major else False
687
+
688
+def check_windows():
689
+	return True if os.name == 'nt' else False
690
+
691
+def clear():
692
+	os.system('cls') if check_windows() else os.system('clear')
693
+
694
+def error(msg, reason=None):
695
+	logging.error(f'[!] - {msg} ({reason})') if reason else logging.error('[!] - ' + msg)
696
+
697
+def error_exit(msg):
698
+	raise SystemExit('[!] - ' + msg)
699
+
700
+def info():
701
+	clear()
702
+	print('#'*56)
703
+	print('#{0}#'.format(''.center(54)))
704
+	print('#{0}#'.format('Scroll IRC Bot'.center(54)))
705
+	print('#{0}#'.format('Developed by acidvegas in Python'.center(54)))
706
+	print('#{0}#'.format('https://acid.vegas/scroll'.center(54)))
707
+	print('#{0}#'.format(''.center(54)))
708
+	print('#'*56)
709
+
710
+def irc(msg):
711
+	logging.debug('[~] - ' + msg)
712
+
713
+def setup_logger():
714
+	stream_handler = logging.StreamHandler(sys.stdout)
715
+	if config.settings.log:
716
+		log_file     = os.path.join(os.path.join('data','logs'), 'scroll.log')
717
+		file_handler = RotatingFileHandler(log_file, maxBytes=256000, backupCount=3)
718
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(file_handler,stream_handler))
719
+	else:
720
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(stream_handler,))
721
diff --git a/scroll/core/functions.py b/scroll/core/functions.py
722
new file mode 100644
723
index 0000000..148a5d7
724
--- /dev/null
725
+++ b/scroll/core/functions.py
726
@@ -0,0 +1,58 @@
727
+#!/usr/bin/env python
728
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
729
+# functions.py
730
+
731
+import base64
732
+import json
733
+import os
734
+import random
735
+import re
736
+import subprocess
737
+import urllib.parse
738
+import urllib.request
739
+
740
+import config
741
+
742
+def cmd(command):
743
+	return subprocess.check_output(command, shell=True).decode()
744
+
745
+def check_trunc(trunc):
746
+	trunc = trunc.split(',')
747
+	if len(trunc) == 5 and ''.join(trunc).isdigit():
748
+		trunc = [int(item) for item in trunc]
749
+		up,down,left,right,space=trunc
750
+		if down == 0:
751
+			down = 1
752
+		if right == 0:
753
+			right = 1
754
+		return trunc
755
+	else:
756
+		return False
757
+
758
+def get_size(file_path):
759
+	return os.path.getsize(file_path)
760
+
761
+def get_source(url):
762
+	return urllib.request.urlopen(url, timeout=15).read().decode('utf8')
763
+
764
+def imgur_upload(file_path):
765
+	data = urllib.parse.urlencode({'image':base64.b64encode(open(file_path,'rb').read()),'key':config.IMGUR_API_KEY,'name':'ASCII uploaded via Scroll','title':'ASCII uploaded via Scroll','type':'base64'}).encode('utf8')
766
+	headers = {'Authorization':'Client-ID ' + config.IMGUR_API_KEY}
767
+	req = urllib.request.Request('https://api.imgur.com/3/upload.json', data, headers)
768
+	response = json.loads(urllib.request.urlopen(req).read())
769
+	return response['data']['link']
770
+
771
+def is_admin(ident):
772
+	return re.compile(config.settings.admin.replace('*','.*')).search(ident)
773
+
774
+def floatint(data):
775
+	if data.isdigit():
776
+		return int(data)
777
+	else:
778
+		try:
779
+			return float(data)
780
+		except:
781
+			return data
782
+
783
+def random_int(min, max):
784
+	return random.randint(min, max)
785
diff --git a/scroll/core/irc.py b/scroll/core/irc.py
786
new file mode 100644
787
index 0000000..51f2fd7
788
--- /dev/null
789
+++ b/scroll/core/irc.py
790
@@ -0,0 +1,354 @@
791
+#!/usr/bin/env python
792
+# Scroll IRC Bot - Developed by acidvegas in Python (https://acid.vegas/scroll)
793
+# irc.py
794
+
795
+import glob
796
+import os
797
+import random
798
+import socket
799
+import threading
800
+import time
801
+
802
+import ascii2png
803
+import config
804
+import constants
805
+import database
806
+import debug
807
+import functions
808
+
809
+# Load optional modules
810
+if config.connection.ssl:
811
+	import ssl
812
+if config.connection.proxy:
813
+	try:
814
+		import sock
815
+	except ImportError:
816
+		debug.error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') # Required for proxy support.
817
+
818
+def color(msg, foreground, background=None):
819
+	return f'\x03{foreground},{background}{msg}{constants.reset}' if background else f'\x03{foreground}{msg}{constants.reset}'
820
+
821
+ascii_dir = os.path.join('data', 'art')
822
+
823
+class IRC(object):
824
+	def __init__(self):
825
+		self.last     = 0
826
+		self.last_png = 0
827
+		self.playing  = False
828
+		self.slow     = False
829
+		self.stopper  = False
830
+		self.sock     = None
831
+
832
+	def connect(self):
833
+		try:
834
+			self.create_socket()
835
+			self.sock.connect((config.connection.server, config.connection.port))
836
+			self.register()
837
+		except socket.error as ex:
838
+			debug.error('Failed to connect to IRC server.', ex)
839
+			Events.disconnect()
840
+		else:
841
+			self.listen()
842
+
843
+	def create_socket(self):
844
+		family = socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET
845
+		if config.connection.proxy:
846
+			proxy_server, proxy_port = config.connection.proxy.split(':')
847
+			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
848
+			self.sock.setblocking(0)
849
+			self.sock.settimeout(15)
850
+			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
851
+		else:
852
+			self.sock = socket.socket(family, socket.SOCK_STREAM)
853
+		if config.connection.vhost:
854
+			self.sock.bind((config.connection.vhost, 0))
855
+		if config.connection.ssl:
856
+			ctx = ssl.create_default_context()
857
+			if config.cert.file:
858
+				ctx.load_cert_chain(config.cert.file, config.cert.key, config.cert.password)
859
+			if config.connection.ssl_verify:
860
+				ctx.verify_mode = ssl.CERT_REQUIRED
861
+				ctx.load_default_certs()
862
+			else:
863
+				ctx.check_hostname = False
864
+				ctx.verify_mode = ssl.CERT_NONE
865
+			self.sock = ctx.wrap_socket(self.sock)
866
+
867
+	def listen(self):
868
+		while True:
869
+			try:
870
+				data = self.sock.recv(1024).decode('utf-8')
871
+				for line in (line for line in data.split('\r\n') if len(line.split()) >= 2):
872
+					debug.irc(line)
873
+					Events.handle(line)
874
+			except (UnicodeDecodeError, UnicodeEncodeError):
875
+				pass
876
+			except Exception as ex:
877
+				debug.error('Unexpected error occured.', ex)
878
+				break
879
+		Events.disconnect()
880
+
881
+	def register(self):
882
+		if config.login.network:
883
+			Commands.raw('PASS ' + config.login.network)
884
+		Commands.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}')
885
+		Commands.raw('NICK '+ config.ident.nickname)
886
+
887
+class Commands:
888
+	def error(chan, msg, reason=None):
889
+		if reason:
890
+			Commands.sendmsg(chan, '[{0}] {1} {2}'.format(color('ERROR', constants.red), msg, color(f'({reason})', constants.grey)))
891
+		else:
892
+			Commands.sendmsg(chan, '[{0}] {1}'.format(color('ERROR', constants.red), msg))
893
+
894
+	def join_channel(chan, key=None):
895
+		Commands.raw(f'JOIN {chan} {key}') if key else Commands.raw('JOIN ' + chan)
896
+
897
+	def play(chan, ascii_file, trunc=None):
898
+		try:
899
+			Bot.playing = True
900
+			data = open(ascii_file, encoding='utf8', errors='replace').read()
901
+			if len(data.splitlines()) > int(database.Settings.get('max_lines')) and chan != '#scroll':
902
+				Commands.error(chan, 'File is too big.', 'Take it to #scroll')
903
+			else:
904
+				name = ascii_file.split(ascii_dir)[1]
905
+				data = data.splitlines()[trunc[0]:-trunc[1]] if trunc else data.splitlines()
906
+				Commands.sendmsg(chan, ascii_file.split(ascii_dir)[1])
907
+				for line in (line for line in data if line):
908
+					if Bot.stopper:
909
+						break
910
+					elif trunc:
911
+						line = line[trunc[2]:-trunc[3]]
912
+					Commands.sendmsg(chan, ' '*trunc[4] + line) if trunc else Commands.sendmsg(chan, line)
913
+		except Exception as ex:
914
+			debug.error('Error occured in the play function!', ex)
915
+		finally:
916
+			Bot.stopper = False
917
+			Bot.playing = False
918
+
919
+	def raw(msg):
920
+		Bot.sock.send(bytes(msg + '\r\n', 'utf-8'))
921
+
922
+	def sendmsg(target, msg):
923
+		Commands.raw(f'PRIVMSG {target} :{msg}')
924
+		time.sleep(functions.floatint(database.Settings.get('msg_throttle')))
925
+
926
+class Events:
927
+	def connect():
928
+		if config.settings.modes:
929
+			Commands.raw(f'MODE {config.ident.nickname} +{config.settings.modes}')
930
+		if config.login.nickserv:
931
+			Commands.sendmsg('NickServ', f'IDENTIFY {config.ident.nickname} {config.login.nickserv}')
932
+		if config.login.operator:
933
+			Commands.raw(f'OPER {config.ident.username} {config.login.operator}')
934
+		Commands.join_channel(config.connection.channel, config.connection.key)
935
+		Commands.join_channel('#scroll')
936
+
937
+	def disconnect():
938
+		Bot.sock.close()
939
+		time.sleep(15)
940
+		Bot.connect()
941
+
942
+	def kick(chan, kicked):
943
+		if kicked == config.ident.nickname:
944
+			if chan == config.connection.channel:
945
+				time.sleep(3)
946
+				Commands.join_channel(chan, config.connection.key)
947
+			elif chan == '#scroll':
948
+				time.sleep(3)
949
+				Commands.join_channel(chan)
950
+
951
+	def message(ident, nick, chan, msg):
952
+		try:
953
+			args = msg.split()
954
+			if msg == '@scroll':
955
+				Commands.sendmsg(chan, constants.bold + 'Scroll IRC Bot - Developed by acidvegas in Python - https://acid.vegas/scroll')
956
+			elif args[0] == '.ascii' and not database.Ignore.check(ident):
957
+				if Bot.playing and msg == '.ascii stop':
958
+					Bot.stopper = True
959
+				elif time.time() - Bot.last < int(database.Settings.get('cmd_throttle')) and not functions.is_admin(ident):
960
+					if not Bot.slow:
961
+						Commands.error(chan, 'Slow down nerd!')
962
+						Bot.slow = True
963
+				elif len(args) >= 2:
964
+					Bot.slow = False
965
+					if args[1] == 'dirs' and len(args) == 2:
966
+						dirs = sorted(glob.glob(os.path.join(ascii_dir, '*/')))
967
+						for directory in dirs:
968
+							name = os.path.basename(os.path.dirname(directory))
969
+							file_count = str(len(glob.glob(os.path.join(directory, '*.txt'))))
970
+							Commands.sendmsg(chan, '[{0}] {1} {2}'.format(color(str(dirs.index(directory)+1).zfill(2), constants.pink), name.ljust(10), color(f'({file_count})', constants.grey)))
971
+					elif args[1] == 'png' and len(args) == 3:
972
+						url = args[2]
973
+						if url.startswith('https://pastebin.com/raw/') or url.startswith('http://termbin.com/'):
974
+							ascii2png.ascii_png(url)
975
+							ascii_file = os.path.join('data','temp.png')
976
+							if os.path.getsize(ascii_file) < int(database.Settings.get('png_max_bytes')):
977
+								Commands.sendmsg(chan, functions.imgur_upload(os.path.join('data','temp.png')))
978
+								Bot.last_png = time.time()
979
+							else:
980
+								Commands.error(chan, 'File too large', '2MB Max')
981
+						else:
982
+							Commands.error(chan, 'Invalid URL.', 'Only PasteBin & TermBin URLs can be used.')
983
+					elif args[1] == 'remote' and len(args) == 3:
984
+						url = args[2]
985
+						if 'pastebin.com/raw/' in url or 'termbin.com/' in url:
986
+							data = functions.get_source(url)
987
+							if data:
988
+								for item in data.split('\n'):
989
+									Commands.sendmsg(chan, item)
990
+							else:
991
+								Commands.error(chan, 'Found no data on URL!')
992
+						else:
993
+							Commands.error(chan, 'Invalid URL!', 'Must be a pastebin.com or termbin.com link.')
994
+					elif args[1] == 'search' and len(args) == 3:
995
+						query   = args[2]
996
+						results = glob.glob(os.path.join(ascii_dir, f'**/*{query}*.txt'), recursive=True)
997
+						if results:
998
+							results = results[:int(database.Settings.get('max_results'))]
999
+							for file_name in results: