Browse Source

Initial commit.

Dionysus 11 months ago
commit
4e4d8a9ff5
No known key found for this signature in database

+ 15
- 0
LICENSE View File

@@ -0,0 +1,15 @@
1
+ISC License
2
+
3
+Copyright (c) 2019, acidvegas <acid.vegas@acid.vegas>
4
+
5
+Permission to use, copy, modify, and/or distribute this software for any
6
+purpose with or without fee is hereby granted, provided that the above
7
+copyright notice and this permission notice appear in all copies.
8
+
9
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 73
- 0
README.md View File

@@ -0,0 +1,73 @@
1
+# scroll
2
+![](screens/art.png)
3
+
4
+A bot to play ASCII art for the Internet Relay Chat (IRC) protocol.
5
+
6
+###### Requirements
7
+* [Python](https://www.python.org/downloads/) *(**Note:** This script was developed to be used with the latest version of Python.)*
8
+* [Pillow](https://pypi.org/project/Pillow/)
9
+
10
+###### Information
11
+Place your ASCII files in the [data/ascii]https://git.supernets.org/acidvegas/scroll/src/branch/master/scroll/data/ascii) directory.
12
+
13
+A large organized ASCII art pack is included in the [ircart](https://git.supernets.org/acidvegas/ircart) repository.
14
+
15
+The `.update` command requires that you `git clone` the repository above to the ASCII dir.
16
+
17
+###### Trunc
18
+The \<trunc> argument for the `.ascii` command allows you to truncate lines off of an ASCII.
19
+
20
+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.
21
+
22
+**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.)*
23
+
24
+###### Commands
25
+| Command | Description |
26
+| --- | --- |
27
+| @scroll | Information about the bot. |
28
+| .ascii dirs | List directories in the ASCII folder. |
29
+| .ascii list | Send a link to a list of the ASCII files. |
30
+| .ascii png \<url> | Convert ASCII to a PNG and upload it to IMGUR. *(The \<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link.) |
31
+| .ascii random [dir] | Play a random ASCII file, optionally from the [dir] directory only. |
32
+| .ascii remote \<url> | Play a remote ASCII file. *(The \<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link.) |
33
+| .ascii search \<query> | Search through the ASCII files for \<query>. |
34
+| .ascii stop | Stop playing the current ASCII being scrolled. |
35
+| .ascii upload [\<url> \<title>] | List uploaded files or upload \<url> as \<title>. *(The \<url> must be a [Pastebin](https://pastebin.com/) or [Termbin](https://termbin.com/) link.)* |
36
+| .ascii \<name> [\<trunc>] | Play the \<name> ASCII file. *(See above about \<trunc>)* |
37
+
38
+###### Admin Commands (Private Message)
39
+| Command | Description |
40
+| --- | --- |
41
+| .config [\<setting> \<value>] | View the config settings or change \<setting> to \<value>. |
42
+| .ignore [\<add/del> \<ident>] | View the ignore list or \<add/del> an \<ident> to the ignore list. *(Must be in nick!user@host format. Wildcards accepted.)*|
43
+| .raw \<data> | Send RAW \<data> to the server. |
44
+| .update | Update the internal [ircart](https://git.supernets.org/acidvegas/ircart) repository. |
45
+
46
+###### Config Settings
47
+| Setting | Description |
48
+| --- | --- |
49
+| cmd_throttle | Delay between command usage for flood control. |
50
+| max_lines | Maximum number of lines an ASCII can be to be played outside of the *#scroll* channel. |
51
+| max_png_size | Maximum size (in byte) for `.ascii png` usage.
52
+| max_results | Maximum number of results returned from a search. |
53
+| max_uploads | Maximum number of uploads to store. |
54
+| max_uploads_per | Maximum number of uploads per-nick. |
55
+| msg_throttle | Delay between each line send to the channel. |
56
+| png_throttle | Delay between `.ascii png` usage. |
57
+| png_max_bytes | Maximum bytes for `.ascii png` uploads. |
58
+| rnd_exclude | Directories to ignore with `.ascii random`. *(Comma-seperated list)* |
59
+
60
+###### Todo
61
+* Check pastebin size before downloading.
62
+* Store image bytes in memory so creating files locally is not required.
63
+* Improve truncation of the left and right by retaining the last known color code.
64
+* Add ascii tagging to database. Lets you tag asciis with hashtags for searching. Maybe include an author tag also.
65
+* Add an ascii amplification factor to the trunc array, and maybe rename trunc to morph.
66
+* Use the maximum allowed upload size for IMGUR upload, and track it to a maximum of 50 per hour. 
67
+* Controls for admins to move, rename, delete, and download ascii on the fly.
68
+
69
+###### Mirrors
70
+- [acid.vegas](https://git.acid.vegas/scroll) *(main)*
71
+- [SuperNETs](https://git.supernets.org/ircart/scroll)
72
+- [GitHub](https://github.com/ircart/scroll)
73
+- [GitLab](https://gitlab.com/ircart/scroll)

BIN
screens/art.png View File


+ 172
- 0
scroll/core/ascii2png.py View File

@@ -0,0 +1,172 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+# Scroll IRC Bot
4
+# Developed by acidvegas in Python
5
+# https://git.acid.vegas/scroll
6
+# ascii2png.py
7
+
8
+'''
9
+Requires:
10
+	https://pypi.org/project/Pillow/
11
+
12
+Credits to VXP for making the original "pngbot" script:
13
+	https://github.com/lalbornoz/MiRCARTools
14
+'''
15
+
16
+import os
17
+import urllib.request
18
+
19
+from PIL import Image, ImageDraw, ImageFont
20
+
21
+def flip_cell_state(cellState, bit):
22
+	if cellState & bit:
23
+		return cellState & ~bit
24
+	else:
25
+		return cellState | bit
26
+
27
+def parse_char(colourSpec, curColours):
28
+	if len(colourSpec) > 0:
29
+		colourSpec = colourSpec.split(',')
30
+		if len(colourSpec) == 2 and len(colourSpec[1]) > 0:
31
+			return (int(colourSpec[0] or curColours[0]), int(colourSpec[1]))
32
+		elif len(colourSpec) == 1 or len(colourSpec[1]) == 0:
33
+			return (int(colourSpec[0]), curColours[1])
34
+	else:
35
+		return (15, 1)
36
+
37
+def ascii_png(url):
38
+	text_file = os.path.join('data','temp.txt')
39
+	if os.path.isfile(text_file):
40
+		os.remove(text_file)
41
+	urllib.request.urlretrieve(url, text_file)
42
+	data = open(text_file)
43
+	inCurColourSpec = ''
44
+	inCurRow = -1
45
+	inLine = data.readline()
46
+	inSize = [0, 0]
47
+	inMaxCols = 0
48
+	outMap = []
49
+	while inLine:
50
+		inCellState = 0x00
51
+		inParseState = 1
52
+		inCurCol = 0
53
+		inMaxCol = len(inLine)
54
+		inCurColourDigits = 0
55
+		inCurColours = (15, 1)
56
+		inCurColourSpec = ''
57
+		inCurRow += 1
58
+		outMap.append([])
59
+		inRowCols = 0
60
+		inSize[1] += 1
61
+		while inCurCol < inMaxCol:
62
+			inChar = inLine[inCurCol]
63
+			if inChar in set('\r\n'):
64
+				inCurCol += 1
65
+			elif inParseState == 1:
66
+				inCurCol += 1
67
+				if inChar == '':
68
+					inCellState = flip_cell_state(inCellState, 0x01)
69
+				elif inChar == '':
70
+					inParseState = 2
71
+				elif inChar == '':
72
+					inCellState = flip_cell_state(inCellState, 0x02)
73
+				elif inChar == '':
74
+					inCellState |= 0x00
75
+					inCurColours = (15, 1)
76
+				elif inChar == '':
77
+					inCurColours = (inCurColours[1], inCurColours[0])
78
+				elif inChar == '':
79
+					inCellState = flip_cell_state(inCellState, 0x04)
80
+				else:
81
+					inRowCols += 1
82
+					outMap[inCurRow].append([*inCurColours, inCellState, inChar])
83
+			elif inParseState == 2 or inParseState == 3:
84
+				if inChar == ',' and inParseState == 2:
85
+					if (inCurCol + 1) < inMaxCol and not inLine[inCurCol + 1] in set('0123456789'):
86
+						inCurColours = parse_char(inCurColourSpec, inCurColours)
87
+						inCurColourDigits = 0
88
+						inCurColourSpec = ''
89
+						inParseState = 1
90
+					else:
91
+						inCurCol += 1
92
+						inCurColourDigits = 0
93
+						inCurColourSpec += inChar
94
+						inParseState = 3
95
+				elif inChar in set('0123456789') and inCurColourDigits == 0:
96
+					inCurCol += 1
97
+					inCurColourDigits += 1
98
+					inCurColourSpec += inChar
99
+				elif inChar in set('0123456789') and inCurColourDigits == 1 and inCurColourSpec[-1] == '0':
100
+					inCurCol += 1
101
+					inCurColourDigits += 1
102
+					inCurColourSpec += inChar
103
+				elif inChar in set('012345') and inCurColourDigits == 1 and inCurColourSpec[-1] == '1':
104
+					inCurCol += 1
105
+					inCurColourDigits += 1
106
+					inCurColourSpec += inChar
107
+				else:
108
+					inCurColours = parse_char(inCurColourSpec, inCurColours)
109
+					inCurColourDigits = 0
110
+					inCurColourSpec = ''
111
+					inParseState = 1
112
+		inMaxCols = max(inMaxCols, inRowCols)
113
+		inLine = data.readline()
114
+	inSize[0] = inMaxCols
115
+	canvas_data = outMap
116
+	numRowCols = 0
117
+	for numRow in range(len(outMap)):
118
+		numRowCols = max(numRowCols, len(outMap[numRow]))
119
+	for numRow in range(len(outMap)):
120
+		if len(outMap[numRow]) != numRowCols:
121
+			for numColOff in range(numRowCols - len(outMap[numRow])):
122
+				outMap[numRow].append([1,1,0,' '])
123
+		outMap[numRow].insert(0,[1,1,0,' '])
124
+		outMap[numRow].append([1,1,0,' '])
125
+	outMap.insert(0,[[1,1,0,' ']] * len(outMap[0]))
126
+	outMap.append([[1,1,0,' ']] * len(outMap[0]))
127
+	inCanvasMap = outMap
128
+	outImgFont = ImageFont.truetype(os.path.join('data','DejaVuSansMono.ttf'), 11)
129
+	outImgFontSize = [*outImgFont.getsize(' ')]
130
+	outImgFontSize[1] += 3
131
+	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]]
132
+	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]]
133
+	inSize = (len(inCanvasMap[0]), len(inCanvasMap))
134
+	outSize = [a*b for a,b in zip(inSize, outImgFontSize)]
135
+	outCurPos = [0, 0]
136
+	outImg = Image.new('RGBA', outSize, (*ColorsNormal[1], 255))
137
+	outImgDraw = ImageDraw.Draw(outImg)
138
+	for inCurRow in range(len(inCanvasMap)):
139
+		for inCurCol in range(len(inCanvasMap[inCurRow])):
140
+			inCurCell = inCanvasMap[inCurRow][inCurCol]
141
+			outColours = [0, 0]
142
+			if inCurCell[2] & 0x01:
143
+				if inCurCell[3] != ' ':
144
+					if inCurCell[3] == '█':
145
+						outColours[1] = ColorsNormal[inCurCell[0]]
146
+					else:
147
+						outColours[0] = ColorsBold[inCurCell[0]]
148
+						outColours[1] = ColorsNormal[inCurCell[1]]
149
+				else:
150
+					outColours[1] = ColorsNormal[inCurCell[1]]
151
+			else:
152
+				if inCurCell[3] != ' ':
153
+					if inCurCell[3] == '█':
154
+						outColours[1] = ColorsNormal[inCurCell[0]]
155
+					else:
156
+						outColours[0] = ColorsNormal[inCurCell[0]]
157
+						outColours[1] = ColorsNormal[inCurCell[1]]
158
+				else:
159
+					outColours[1] = ColorsNormal[inCurCell[1]]
160
+			outImgDraw.rectangle((*outCurPos,outCurPos[0] + outImgFontSize[0], outCurPos[1] + outImgFontSize[1]), fill=(*outColours[1], 255))
161
+			if  not inCurCell[3] in ' █' and outColours[0] != outColours[1]:
162
+				outImgDraw.text(outCurPos,inCurCell[3], (*outColours[0], 255), outImgFont)
163
+			if inCurCell[2] & 0x04:
164
+				outColours[0] = ColorsNormal[inCurCell[0]]
165
+				outImgDraw.line(xy=(outCurPos[0], outCurPos[1] + (outImgFontSize[1] - 2), outCurPos[0] + outImgFontSize[0], outCurPos[1] + (outImgFontSize[1] - 2)), fill=(*outColours[0], 255))
166
+			outCurPos[0] += outImgFontSize[0]
167
+		outCurPos[0] = 0
168
+		outCurPos[1] += outImgFontSize[1]
169
+	out_file = os.path.join('data','temp.png')
170
+	if os.path.isfile(out_file):
171
+		os.remove(out_file)
172
+	outImg.save(out_file)

+ 38
- 0
scroll/core/config.py View File

@@ -0,0 +1,38 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# config.py
6
+
7
+class connection:
8
+	server     = 'irc.server.com'
9
+	port       = 6697
10
+	proxy      = None
11
+	ipv6       = False
12
+	ssl        = True
13
+	ssl_verify = False
14
+	vhost      = None
15
+	channel    = '#chats'
16
+	key        = None
17
+
18
+class cert:
19
+	key      = None
20
+	file     = None
21
+	password = None
22
+
23
+class ident:
24
+	nickname = 'scroll'
25
+	username = 'scroll'
26
+	realname = 'git.acid.vegas/ircart'
27
+
28
+class login:
29
+	network  = None
30
+	nickserv = None
31
+	operator = None
32
+
33
+class settings:
34
+	admin = 'nick!user@host' # Must be in nick!user@host format (Can use wildcards here)
35
+	log   = False
36
+	modes = None
37
+
38
+IMGUR_API_KEY = 'CHANGEME' # https://apidocs.imgur.com/

+ 231
- 0
scroll/core/constants.py View File

@@ -0,0 +1,231 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# constants.py
6
+
7
+# Control Characters
8
+bold      = '\x02'
9
+color     = '\x03'
10
+italic    = '\x1D'
11
+underline = '\x1F'
12
+reverse   = '\x16'
13
+reset     = '\x0f'
14
+
15
+# Color Codes
16
+white       = '00'
17
+black       = '01'
18
+blue        = '02'
19
+green       = '03'
20
+red         = '04'
21
+brown       = '05'
22
+purple      = '06'
23
+orange      = '07'
24
+yellow      = '08'
25
+light_green = '09'
26
+cyan        = '10'
27
+light_cyan  = '11'
28
+light_blue  = '12'
29
+pink        = '13'
30
+grey        = '14'
31
+light_grey  = '15'
32
+
33
+# Events
34
+PASS     = 'PASS'
35
+NICK     = 'NICK'
36
+USER     = 'USER'
37
+OPER     = 'OPER'
38
+MODE     = 'MODE'
39
+SERVICE  = 'SERVICE'
40
+QUIT     = 'QUIT'
41
+SQUIT    = 'SQUIT'
42
+JOIN     = 'JOIN'
43
+PART     = 'PART'
44
+TOPIC    = 'TOPIC'
45
+NAMES    = 'NAMES'
46
+LIST     = 'LIST'
47
+INVITE   = 'INVITE'
48
+KICK     = 'KICK'
49
+PRIVMSG  = 'PRIVMSG'
50
+NOTICE   = 'NOTICE'
51
+MOTD     = 'MOTD'
52
+LUSERS   = 'LUSERS'
53
+VERSION  = 'VERSION'
54
+STATS    = 'STATS'
55
+LINKS    = 'LINKS'
56
+TIME     = 'TIME'
57
+CONNECT  = 'CONNECT'
58
+TRACE    = 'TRACE'
59
+ADMIN    = 'ADMIN'
60
+INFO     = 'INFO'
61
+SERVLIST = 'SERVLIST'
62
+SQUERY   = 'SQUERY'
63
+WHO      = 'WHO'
64
+WHOIS    = 'WHOIS'
65
+WHOWAS   = 'WHOWAS'
66
+KILL     = 'KILL'
67
+PING     = 'PING'
68
+PONG     = 'PONG'
69
+ERROR    = 'ERROR'
70
+AWAY     = 'AWAY'
71
+REHASH   = 'REHASH'
72
+DIE      = 'DIE'
73
+RESTART  = 'RESTART'
74
+SUMMON   = 'SUMMON'
75
+USERS    = 'USERS'
76
+WALLOPS  = 'WALLOPS'
77
+USERHOST = 'USERHOST'
78
+ISON     = 'ISON'
79
+
80
+# Event Numerics
81
+RPL_WELCOME          = '001'
82
+RPL_YOURHOST         = '002'
83
+RPL_CREATED          = '003'
84
+RPL_MYINFO           = '004'
85
+RPL_ISUPPORT         = '005'
86
+RPL_TRACELINK        = '200'
87
+RPL_TRACECONNECTING  = '201'
88
+RPL_TRACEHANDSHAKE   = '202'
89
+RPL_TRACEUNKNOWN     = '203'
90
+RPL_TRACEOPERATOR    = '204'
91
+RPL_TRACEUSER        = '205'
92
+RPL_TRACESERVER      = '206'
93
+RPL_TRACESERVICE     = '207'
94
+RPL_TRACENEWTYPE     = '208'
95
+RPL_TRACECLASS       = '209'
96
+RPL_STATSLINKINFO    = '211'
97
+RPL_STATSCOMMANDS    = '212'
98
+RPL_STATSCLINE       = '213'
99
+RPL_STATSILINE       = '215'
100
+RPL_STATSKLINE       = '216'
101
+RPL_STATSYLINE       = '218'
102
+RPL_ENDOFSTATS       = '219'
103
+RPL_UMODEIS          = '221'
104
+RPL_SERVLIST         = '234'
105
+RPL_SERVLISTEND      = '235'
106
+RPL_STATSLLINE       = '241'
107
+RPL_STATSUPTIME      = '242'
108
+RPL_STATSOLINE       = '243'
109
+RPL_STATSHLINE       = '244'
110
+RPL_LUSERCLIENT      = '251'
111
+RPL_LUSEROP          = '252'
112
+RPL_LUSERUNKNOWN     = '253'
113
+RPL_LUSERCHANNELS    = '254'
114
+RPL_LUSERME          = '255'
115
+RPL_ADMINME          = '256'
116
+RPL_ADMINLOC1        = '257'
117
+RPL_ADMINLOC2        = '258'
118
+RPL_ADMINEMAIL       = '259'
119
+RPL_TRACELOG         = '261'
120
+RPL_TRYAGAIN         = '263'
121
+RPL_NONE             = '300'
122
+RPL_AWAY             = '301'
123
+RPL_USERHOST         = '302'
124
+RPL_ISON             = '303'
125
+RPL_UNAWAY           = '305'
126
+RPL_NOWAWAY          = '306'
127
+RPL_WHOISUSER        = '311'
128
+RPL_WHOISSERVER      = '312'
129
+RPL_WHOISOPERATOR    = '313'
130
+RPL_WHOWASUSER       = '314'
131
+RPL_ENDOFWHO         = '315'
132
+RPL_WHOISIDLE        = '317'
133
+RPL_ENDOFWHOIS       = '318'
134
+RPL_WHOISCHANNELS    = '319'
135
+RPL_LIST             = '322'
136
+RPL_LISTEND          = '323'
137
+RPL_CHANNELMODEIS    = '324'
138
+RPL_NOTOPIC          = '331'
139
+RPL_TOPIC            = '332'
140
+RPL_INVITING         = '341'
141
+RPL_INVITELIST       = '346'
142
+RPL_ENDOFINVITELIST  = '347'
143
+RPL_EXCEPTLIST       = '348'
144
+RPL_ENDOFEXCEPTLIST  = '349'
145
+RPL_VERSION          = '351'
146
+RPL_WHOREPLY         = '352'
147
+RPL_NAMREPLY         = '353'
148
+RPL_LINKS            = '364'
149
+RPL_ENDOFLINKS       = '365'
150
+RPL_ENDOFNAMES       = '366'
151
+RPL_BANLIST          = '367'
152
+RPL_ENDOFBANLIST     = '368'
153
+RPL_ENDOFWHOWAS      = '369'
154
+RPL_INFO             = '371'
155
+RPL_MOTD             = '372'
156
+RPL_ENDOFINFO        = '374'
157
+RPL_MOTDSTART        = '375'
158
+RPL_ENDOFMOTD        = '376'
159
+RPL_YOUREOPER        = '381'
160
+RPL_REHASHING        = '382'
161
+RPL_YOURESERVICE     = '383'
162
+RPL_TIME             = '391'
163
+RPL_USERSSTART       = '392'
164
+RPL_USERS            = '393'
165
+RPL_ENDOFUSERS       = '394'
166
+RPL_NOUSERS          = '395'
167
+ERR_NOSUCHNICK       = '401'
168
+ERR_NOSUCHSERVER     = '402'
169
+ERR_NOSUCHCHANNEL    = '403'
170
+ERR_CANNOTSENDTOCHAN = '404'
171
+ERR_TOOMANYCHANNELS  = '405'
172
+ERR_WASNOSUCHNICK    = '406'
173
+ERR_TOOMANYTARGETS   = '407'
174
+ERR_NOSUCHSERVICE    = '408'
175
+ERR_NOORIGIN         = '409'
176
+ERR_NORECIPIENT      = '411'
177
+ERR_NOTEXTTOSEND     = '412'
178
+ERR_NOTOPLEVEL       = '413'
179
+ERR_WILDTOPLEVEL     = '414'
180
+ERR_BADMASK          = '415'
181
+ERR_UNKNOWNCOMMAND   = '421'
182
+ERR_NOMOTD           = '422'
183
+ERR_NOADMININFO      = '423'
184
+ERR_FILEERROR        = '424'
185
+ERR_NONICKNAMEGIVEN  = '431'
186
+ERR_ERRONEUSNICKNAME = '432'
187
+ERR_NICKNAMEINUSE    = '433'
188
+ERR_NICKCOLLISION    = '436'
189
+ERR_USERNOTINCHANNEL = '441'
190
+ERR_NOTONCHANNEL     = '442'
191
+ERR_USERONCHANNEL    = '443'
192
+ERR_NOLOGIN          = '444'
193
+ERR_SUMMONDISABLED   = '445'
194
+ERR_USERSDISABLED    = '446'
195
+ERR_NOTREGISTERED    = '451'
196
+ERR_NEEDMOREPARAMS   = '461'
197
+ERR_ALREADYREGISTRED = '462'
198
+ERR_NOPERMFORHOST    = '463'
199
+ERR_PASSWDMISMATCH   = '464'
200
+ERR_YOUREBANNEDCREEP = '465'
201
+ERR_KEYSET           = '467'
202
+ERR_CHANNELISFULL    = '471'
203
+ERR_UNKNOWNMODE      = '472'
204
+ERR_INVITEONLYCHAN   = '473'
205
+ERR_BANNEDFROMCHAN   = '474'
206
+ERR_BADCHANNELKEY    = '475'
207
+ERR_BADCHANMASK      = '476'
208
+ERR_BANLISTFULL      = '478'
209
+ERR_NOPRIVILEGES     = '481'
210
+ERR_CHANOPRIVSNEEDED = '482'
211
+ERR_CANTKILLSERVER   = '483'
212
+ERR_UNIQOPRIVSNEEDED = '485'
213
+ERR_NOOPERHOST       = '491'
214
+ERR_UMODEUNKNOWNFLAG = '501'
215
+ERR_USERSDONTMATCH   = '502'
216
+RPL_STARTTLS         = '670'
217
+ERR_STARTTLS         = '691'
218
+RPL_MONONLINE        = '730'
219
+RPL_MONOFFLINE       = '731'
220
+RPL_MONLIST          = '732'
221
+RPL_ENDOFMONLIST     = '733'
222
+ERR_MONLISTFULL      = '734'
223
+RPL_LOGGEDIN         = '900'
224
+RPL_LOGGEDOUT        = '901'
225
+ERR_NICKLOCKED       = '902'
226
+RPL_SASLSUCCESS      = '903'
227
+ERR_SASLFAIL         = '904'
228
+ERR_SASLTOOLONG      = '905'
229
+ERR_SASLABORTED      = '906'
230
+ERR_SASLALREADY      = '907'
231
+RPL_SASLMECHS        = '908'

+ 76
- 0
scroll/core/database.py View File

@@ -0,0 +1,76 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# database.py
6
+
7
+import os
8
+import re
9
+import sqlite3
10
+
11
+db  = sqlite3.connect(os.path.join('data', 'scroll.db'), check_same_thread=False)
12
+sql = db.cursor()
13
+
14
+def check():
15
+	tables = sql.execute('SELECT name FROM sqlite_master WHERE type=\'table\'').fetchall()
16
+	if not len(tables):
17
+		sql.execute('CREATE TABLE IGNORE (IDENT TEXT NOT NULL);')
18
+		sql.execute('CREATE TABLE SETTINGS (SETTING TEXT NOT NULL, VALUE TEXT NOT NULL);')
19
+		sql.execute('CREATE TABLE UPLOADS (NICK TEXT NOT NULL, URL TEXT NOT NULL, TITLE TEXT NOT NULL);')
20
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('cmd_throttle', '3'))
21
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_lines', '300'))
22
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_results', '10'))
23
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads', '25'))
24
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('max_uploads_per', '5'))
25
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('msg_throttle', '0.03'))
26
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('png_throttle', '0.03'))
27
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('png_max_bytes', '2000000'))
28
+		sql.execute('INSERT INTO SETTINGS (SETTING,VALUE) VALUES (?,?)', ('rnd_exclude', 'ansi,big,birds,hang,pokemon'))
29
+		db.commit()
30
+
31
+class Ignore:
32
+	def add(ident):
33
+		sql.execute('INSERT INTO IGNORE (IDENT) VALUES (?)', (ident,))
34
+		db.commit()
35
+
36
+	def check(ident):
37
+		for ignored_ident in Ignore.read():
38
+			if re.compile(ignored_ident.replace('*','.*')).search(ident):
39
+				return True
40
+		return False
41
+
42
+	def read():
43
+		return [item[0] for item in sql.execute('SELECT IDENT FROM IGNORE').fetchall()]
44
+
45
+	def remove(ident):
46
+		sql.execute('DELETE FROM IGNORE WHERE IDENT=?', (ident,))
47
+		db.commit()
48
+
49
+class Settings:
50
+	def get(setting):
51
+		return sql.execute('SELECT VALUE FROM SETTINGS WHERE SETTING=?', (setting,)).fetchone()[0]
52
+
53
+	def read():
54
+		return sql.execute('SELECT SETTING,VALUE FROM SETTINGS ORDER BY SETTING ASC').fetchall()
55
+
56
+	def settings():
57
+		return list(item[0] for item in sql.execute('SELECT SETTING FROM SETTINGS').fetchall())
58
+
59
+	def update(setting, value):
60
+		sql.execute('UPDATE SETTINGS SET VALUE=? WHERE SETTING=?', (value, setting))
61
+		db.commit()
62
+
63
+class Uploads:
64
+	def add(nick, url, title):
65
+		sql.execute('INSERT INTO UPLOADS (NICK,URL,TITLE) VALUES (?,?,?)', (nick,url,title))
66
+		db.commit()
67
+
68
+	def read(nick=None):
69
+		if nick:
70
+			return sql.execute('SELECT NICK,URL,TITLE FROM UPLOADS WHERE NICK=?', (nick,)).fetchall()
71
+		else:
72
+			return sql.execute('SELECT NICK,URL,TITLE FROM UPLOADS').fetchall()
73
+
74
+	def remove(url):
75
+		sql.execute('DELETE FROM UPLOADS WHERE URL=?', (url,))
76
+		db.commit()

+ 58
- 0
scroll/core/debug.py View File

@@ -0,0 +1,58 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# debug.py
6
+
7
+import ctypes
8
+import logging
9
+import os
10
+import sys
11
+import time
12
+
13
+from logging.handlers import RotatingFileHandler
14
+
15
+import config
16
+
17
+def check_privileges():
18
+	if check_windows():
19
+		return True if ctypes.windll.shell32.IsUserAnAdmin() else False
20
+	else:
21
+		return True if os.getuid() == 0 or os.geteuid() == 0 else False
22
+
23
+def check_version(major):
24
+	return True if sys.version_info.major == major else False
25
+
26
+def check_windows():
27
+	return True if os.name == 'nt' else False
28
+
29
+def clear():
30
+	os.system('cls') if check_windows() else os.system('clear')
31
+
32
+def error(msg, reason=None):
33
+	logging.error(f'[!] - {msg} ({reason})') if reason else logging.error('[!] - ' + msg)
34
+
35
+def error_exit(msg):
36
+	raise SystemExit('[!] - ' + msg)
37
+
38
+def info():
39
+	clear()
40
+	print('#'*56)
41
+	print('#{0}#'.format(''.center(54)))
42
+	print('#{0}#'.format('Scroll IRC Bot'.center(54)))
43
+	print('#{0}#'.format('Developed by acidvegas in Python'.center(54)))
44
+	print('#{0}#'.format('https://git.acid.vegas/scroll'.center(54)))
45
+	print('#{0}#'.format(''.center(54)))
46
+	print('#'*56)
47
+
48
+def irc(msg):
49
+	logging.debug('[~] - ' + msg)
50
+
51
+def setup_logger():
52
+	stream_handler = logging.StreamHandler(sys.stdout)
53
+	if config.settings.log:
54
+		log_file     = os.path.join(os.path.join('data','logs'), 'scroll.log')
55
+		file_handler = RotatingFileHandler(log_file, maxBytes=256000, backupCount=3)
56
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(file_handler,stream_handler))
57
+	else:
58
+		logging.basicConfig(level=logging.NOTSET, format='%(asctime)s | %(message)s', datefmt='%I:%M:%S', handlers=(stream_handler,))

+ 60
- 0
scroll/core/functions.py View File

@@ -0,0 +1,60 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# functions.py
6
+
7
+import base64
8
+import json
9
+import os
10
+import random
11
+import re
12
+import subprocess
13
+import urllib.parse
14
+import urllib.request
15
+
16
+import config
17
+
18
+def cmd(command):
19
+	return subprocess.check_output(command, shell=True).decode()
20
+
21
+def check_trunc(trunc):
22
+	trunc = trunc.split(',')
23
+	if len(trunc) == 5 and ''.join(trunc).isdigit():
24
+		trunc = [int(item) for item in trunc]
25
+		up,down,left,right,space=trunc
26
+		if down == 0:
27
+			down = 1
28
+		if right == 0:
29
+			right = 1
30
+		return trunc
31
+	else:
32
+		return False
33
+
34
+def get_size(file_path):
35
+	return os.path.getsize(file_path)
36
+
37
+def get_source(url):
38
+	return urllib.request.urlopen(url, timeout=15).read().decode('utf8')
39
+
40
+def imgur_upload(file_path):
41
+	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')
42
+	headers = {'Authorization':'Client-ID ' + config.IMGUR_API_KEY}
43
+	req = urllib.request.Request('https://api.imgur.com/3/upload.json', data, headers)
44
+	response = json.loads(urllib.request.urlopen(req).read())
45
+	return response['data']['link']
46
+
47
+def is_admin(ident):
48
+	return re.compile(config.settings.admin.replace('*','.*')).search(ident)
49
+
50
+def floatint(data):
51
+	if data.isdigit():
52
+		return int(data)
53
+	else:
54
+		try:
55
+			return float(data)
56
+		except:
57
+			return data
58
+
59
+def random_int(min, max):
60
+	return random.randint(min, max)

+ 356
- 0
scroll/core/irc.py View File

@@ -0,0 +1,356 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# irc.py
6
+
7
+import glob
8
+import os
9
+import random
10
+import socket
11
+import threading
12
+import time
13
+
14
+import ascii2png
15
+import config
16
+import constants
17
+import database
18
+import debug
19
+import functions
20
+
21
+# Load optional modules
22
+if config.connection.ssl:
23
+	import ssl
24
+if config.connection.proxy:
25
+	try:
26
+		import sock
27
+	except ImportError:
28
+		debug.error_exit('Missing PySocks module! (https://pypi.python.org/pypi/PySocks)') # Required for proxy support.
29
+
30
+def color(msg, foreground, background=None):
31
+	return f'\x03{foreground},{background}{msg}{constants.reset}' if background else f'\x03{foreground}{msg}{constants.reset}'
32
+
33
+ascii_dir = os.path.join('data', 'ascii')
34
+
35
+class IRC(object):
36
+	def __init__(self):
37
+		self.last     = 0
38
+		self.last_png = 0
39
+		self.playing  = False
40
+		self.slow     = False
41
+		self.stopper  = False
42
+		self.sock     = None
43
+
44
+	def connect(self):
45
+		try:
46
+			self.create_socket()
47
+			self.sock.connect((config.connection.server, config.connection.port))
48
+			self.register()
49
+		except socket.error as ex:
50
+			debug.error('Failed to connect to IRC server.', ex)
51
+			Events.disconnect()
52
+		else:
53
+			self.listen()
54
+
55
+	def create_socket(self):
56
+		family = socket.AF_INET6 if config.connection.ipv6 else socket.AF_INET
57
+		if config.connection.proxy:
58
+			proxy_server, proxy_port = config.connection.proxy.split(':')
59
+			self.sock = socks.socksocket(family, socket.SOCK_STREAM)
60
+			self.sock.setblocking(0)
61
+			self.sock.settimeout(15)
62
+			self.sock.setproxy(socks.PROXY_TYPE_SOCKS5, proxy_server, int(proxy_port))
63
+		else:
64
+			self.sock = socket.socket(family, socket.SOCK_STREAM)
65
+		if config.connection.vhost:
66
+			self.sock.bind((config.connection.vhost, 0))
67
+		if config.connection.ssl:
68
+			ctx = ssl.create_default_context()
69
+			if config.cert.file:
70
+				ctx.load_cert_chain(config.cert.file, config.cert.key, config.cert.password)
71
+			if config.connection.ssl_verify:
72
+				ctx.verify_mode = ssl.CERT_REQUIRED
73
+				ctx.load_default_certs()
74
+			else:
75
+				ctx.check_hostname = False
76
+				ctx.verify_mode = ssl.CERT_NONE
77
+			self.sock = ctx.wrap_socket(self.sock)
78
+
79
+	def listen(self):
80
+		while True:
81
+			try:
82
+				data = self.sock.recv(1024).decode('utf-8')
83
+				for line in (line for line in data.split('\r\n') if len(line.split()) >= 2):
84
+					debug.irc(line)
85
+					Events.handle(line)
86
+			except (UnicodeDecodeError, UnicodeEncodeError):
87
+				pass
88
+			except Exception as ex:
89
+				debug.error('Unexpected error occured.', ex)
90
+				break
91
+		Events.disconnect()
92
+
93
+	def register(self):
94
+		if config.login.network:
95
+			Commands.raw('PASS ' + config.login.network)
96
+		Commands.raw(f'USER {config.ident.username} 0 * :{config.ident.realname}')
97
+		Commands.raw('NICK '+ config.ident.nickname)
98
+
99
+class Commands:
100
+	def error(chan, msg, reason=None):
101
+		if reason:
102
+			Commands.sendmsg(chan, '[{0}] {1} {2}'.format(color('ERROR', constants.red), msg, color(f'({reason})', constants.grey)))
103
+		else:
104
+			Commands.sendmsg(chan, '[{0}] {1}'.format(color('ERROR', constants.red), msg))
105
+
106
+	def join_channel(chan, key=None):
107
+		Commands.raw(f'JOIN {chan} {key}') if key else Commands.raw('JOIN ' + chan)
108
+
109
+	def play(chan, ascii_file, trunc=None):
110
+		try:
111
+			Bot.playing = True
112
+			data = open(ascii_file, encoding='utf8', errors='replace').read()
113
+			if len(data.splitlines()) > int(database.Settings.get('max_lines')) and chan != '#scroll':
114
+				Commands.error(chan, 'File is too big.', 'Take it to #scroll')
115
+			else:
116
+				name = ascii_file.split(ascii_dir)[1]
117
+				data = data.splitlines()[trunc[0]:-trunc[1]] if trunc else data.splitlines()
118
+				Commands.sendmsg(chan, ascii_file.split(ascii_dir)[1])
119
+				for line in (line for line in data if line):
120
+					if Bot.stopper:
121
+						break
122
+					elif trunc:
123
+						line = line[trunc[2]:-trunc[3]]
124
+					Commands.sendmsg(chan, ' '*trunc[4] + line) if trunc else Commands.sendmsg(chan, line)
125
+		except Exception as ex:
126
+			debug.error('Error occured in the play function!', ex)
127
+		finally:
128
+			Bot.stopper = False
129
+			Bot.playing = False
130
+
131
+	def raw(msg):
132
+		Bot.sock.send(bytes(msg + '\r\n', 'utf-8'))
133
+
134
+	def sendmsg(target, msg):
135
+		Commands.raw(f'PRIVMSG {target} :{msg}')
136
+		time.sleep(functions.floatint(database.Settings.get('msg_throttle')))
137
+
138
+class Events:
139
+	def connect():
140
+		if config.settings.modes:
141
+			Commands.raw(f'MODE {config.ident.nickname} +{config.settings.modes}')
142
+		if config.login.nickserv:
143
+			Commands.sendmsg('NickServ', f'IDENTIFY {config.ident.nickname} {config.login.nickserv}')
144
+		if config.login.operator:
145
+			Commands.raw(f'OPER {config.ident.username} {config.login.operator}')
146
+		Commands.join_channel(config.connection.channel, config.connection.key)
147
+		Commands.join_channel('#scroll')
148
+
149
+	def disconnect():
150
+		Bot.sock.close()
151
+		time.sleep(15)
152
+		Bot.connect()
153
+
154
+	def kick(chan, kicked):
155
+		if kicked == config.ident.nickname:
156
+			if chan == config.connection.channel:
157
+				time.sleep(3)
158
+				Commands.join_channel(chan, config.connection.key)
159
+			elif chan == '#scroll':
160
+				time.sleep(3)
161
+				Commands.join_channel(chan)
162
+
163
+	def message(ident, nick, chan, msg):
164
+		try:
165
+			args = msg.split()
166
+			if msg == '@scroll':
167
+				Commands.sendmsg(chan, constants.bold + 'Scroll IRC Bot - Developed by acidvegas in Python - https://git.acid.vegas/scroll')
168
+			elif args[0] == '.ascii' and not database.Ignore.check(ident):
169
+				if Bot.playing and msg == '.ascii stop':
170
+					Bot.stopper = True
171
+				elif time.time() - Bot.last < int(database.Settings.get('cmd_throttle')) and not functions.is_admin(ident):
172
+					if not Bot.slow:
173
+						Commands.error(chan, 'Slow down nerd!')
174
+						Bot.slow = True
175
+				elif len(args) >= 2:
176
+					Bot.slow = False
177
+					if args[1] == 'dirs' and len(args) == 2:
178
+						dirs = sorted(glob.glob(os.path.join(ascii_dir, '*/')))
179
+						for directory in dirs:
180
+							name = os.path.basename(os.path.dirname(directory))
181
+							file_count = str(len(glob.glob(os.path.join(directory, '*.txt'))))
182
+							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)))
183
+					elif args[1] == 'png' and len(args) == 3:
184
+						url = args[2]
185
+						if url.startswith('https://pastebin.com/raw/') or url.startswith('http://termbin.com/'):
186
+							ascii2png.ascii_png(url)
187
+							ascii_file = os.path.join('data','temp.png')
188
+							if os.path.getsize(ascii_file) < int(database.Settings.get('png_max_bytes')):
189
+								Commands.sendmsg(chan, functions.imgur_upload(os.path.join('data','temp.png')))
190
+								Bot.last_png = time.time()
191
+							else:
192
+								Commands.error(chan, 'File too large', '2MB Max')
193
+						else:
194
+							Commands.error(chan, 'Invalid URL.', 'Only PasteBin & TermBin URLs can be used.')
195
+					elif args[1] == 'remote' and len(args) == 3:
196
+						url = args[2]
197
+						if 'pastebin.com/raw/' in url or 'termbin.com/' in url:
198
+							data = functions.get_source(url)
199
+							if data:
200
+								for item in data.split('\n'):
201
+									Commands.sendmsg(chan, item)
202
+							else:
203
+								Commands.error(chan, 'Found no data on URL!')
204
+						else:
205
+							Commands.error(chan, 'Invalid URL!', 'Must be a pastebin.com or termbin.com link.')
206
+					elif args[1] == 'search' and len(args) == 3:
207
+						query   = args[2]
208
+						results = glob.glob(os.path.join(ascii_dir, f'**/*{query}*.txt'), recursive=True)
209
+						if results:
210
+							results = results[:int(database.Settings.get('max_results'))]
211
+							for file_name in results:
212
+								count = str(results.index(file_name)+1)
213
+								Commands.sendmsg(chan, '[{0}] {1}'.format(color(count.zfill(2), constants.pink), os.path.basename(file_name)))
214
+						else:
215
+							Commands.error(chan, 'No results found.')
216
+					elif args[1] == 'upload':
217
+						if len(args) == 2:
218
+							uploads = database.Uploads.read()
219
+							if uploads:
220
+								for upload in uploads:
221
+									Commands.sendmsg(chan, '[{0}] {1} - {2} - {3}.txt'.format(color(uploads.index(upload)+1, constants.pink), color(upload[0], constants.yellow), color(upload[1], constants.grey), upload[2]))
222
+							else:
223
+								Commands.error(chan, 'No uploaded files!', 'Use ".ascii upload <url> <title>" to upload.')
224
+						elif len(args) == 4:
225
+							url = args[2]
226
+							if url.startswith('https://pastebin.com/raw/') or url.startswith('https://termbin.com/'):
227
+								if url not in [item[1] for item in database.Uploads.read()]:
228
+									title = args[3].lower()[:20]
229
+									check = (glob.glob(os.path.join(ascii_dir, f'**/{title}.txt'), recursive=True)[:1] or [None])[0]
230
+									if not check:
231
+										if len(database.Uploads.read(nick)) == int(database.Settings.get('max_uploads_per')):
232
+											database.Uploads.remove(database.Uploads.read(nick)[0][1])
233
+										elif len(database.Uploads.read()) == int(database.Settings.get('max_uploads')):
234
+											database.Uploads.remove(database.Uploads.read()[0][1])
235
+										database.Uploads.add(nick, url, title)
236
+										Commands.sendmsg(chan, 'Uploaded!')
237
+									else:
238
+										Commands.error(chan, 'File with that title already exists!')
239
+								else:
240
+									Commands.error(chan, 'URL is already uploaded!')
241
+							else:
242
+								Commands.error(chan, 'Invalid URL.', 'Only PasteBin & TermBin URLs can be used.')
243
+					elif args[1] == 'random':
244
+						if len(args) == 2:
245
+							ascii_file = random.choice([file for file in glob.glob(os.path.join(ascii_dir, '**/*.txt'), recursive=True) if os.path.basename(os.path.dirname(file)) not in database.Settings.get('rnd_exclude').split(',')])
246
+							threading.Thread(target=Commands.play, args=(chan, ascii_file)).start()
247
+						elif len(args) == 3:
248
+							dir = args[2]
249
+							if '../' in dir:
250
+								Commands.error(chan, 'Nice try nerd!')
251
+							elif os.path.isdir(os.path.join(ascii_dir, dir)):
252
+								ascii_file = random.choice(glob.glob(os.path.join(ascii_dir, dir + '/*.txt'), recursive=True))
253
+								threading.Thread(target=Commands.play, args=(chan, ascii_file)).start()
254
+							else:
255
+								Commands.error(chan, 'Invalid directory name.', 'Use ".ascii dirs" for a list of valid directory names.')
256
+					else:
257
+						option = args[1]
258
+						if '/' in option:
259
+							Commands.error(chan, 'Nice try nerd!')
260
+						else:
261
+							ascii_file = (glob.glob(os.path.join(ascii_dir, f'**/{option}.txt'), recursive=True)[:1] or [None])[0]
262
+							if ascii_file:
263
+								if len(args) == 3:
264
+									trunc = functions.check_trunc(args[2])
265
+									if trunc:
266
+										threading.Thread(target=Commands.play, args=(chan, ascii_file, trunc)).start()
267
+									else:
268
+										Commands.error(chan, 'Invalid truncate option.' 'Use TOP,BOTTOM,LEFT,RIGHT,SPACE as integers to truncate.')
269
+								else:
270
+									threading.Thread(target=Commands.play, args=(chan, ascii_file)).start()
271
+							else:
272
+								Commands.error(chan, 'Invalid file name.', 'Use ".ascii list" for a list of valid file names.')
273
+					Bot.last = time.time()
274
+		except Exception as ex:
275
+			if time.time() - Bot.last < int(database.Settings.get('cmd_throttle')):
276
+				if not Bot.slow:
277
+					Commands.sendmsg(chan, color('Slow down nerd!', constants.red))
278
+					Bot.slow = True
279
+			else:
280
+				Commands.error(chan, 'Command threw an exception.', ex)
281
+			Bot.last = time.time()
282
+
283
+	def private(ident, nick, msg):
284
+		if functions.is_admin(ident):
285
+			args = msg.split()
286
+			if msg == '.update':
287
+				output = functions.cmd(f'git -C {ascii_dir} reset --hard FETCH_HEAD')
288
+				if output:
289
+					for line in output.split('\n'):
290
+						Commands.sendmsg(chan, line)
291
+			elif args[0] == '.config':
292
+				if len(args) == 1:
293
+					settings = database.Settings.read()
294
+					Commands.sendmsg(nick, '[{0}]'.format(color('Settings', constants.purple)))
295
+					for setting in settings:
296
+						Commands.sendmsg(nick, '{0} = {1}'.format(color(setting[0], constants.yellow), color(setting[1], constants.grey)))
297
+				elif len(args) == 3:
298
+					setting, value = args[1], args[2]
299
+					if setting in database.Settings.settings():
300
+						database.Settings.update(setting, value)
301
+						Commands.sendmsg(nick, 'Change setting for {0} to {1}.'.format(color(setting, constants.yellow), color(value, constants.grey)))
302
+					else:
303
+						Commands.error(nick, 'Invalid config variable.')
304
+			elif args[0] == '.ignore':
305
+				if len(args) == 1:
306
+					ignores = database.Ignore.read()
307
+					if ignores:
308
+						Commands.sendmsg(nick, '[{0}]'.format(color('Ignore List', constants.purple)))
309
+						for ignore_ident in ignores:
310
+							Commands.sendmsg(nick, color(ignore_ident, constants.yellow))
311
+						Commands.sendmsg(nick, '{0} {1}'.format(color('Total:', constants.light_blue), color(len(ignores), constants.grey)))
312
+					else:
313
+						Commands.error(nick, 'Ignore list is empty!')
314
+				elif len(args) == 2 and args[0] == '.ignore':
315
+					option = args[1][:1]
316
+					if option in '+-':
317
+						ignore_ident = args[1][1:]
318
+						if (not database.Ignore.check(ignore_ident) and option == '+') or (database.Ignore.check(ignore_ident) and option == '-'):
319
+							database.Ignore.add(ignore_ident) if option == '+' else database.Ignore.remove(ignore_host)
320
+							Commands.sendmsg(nick, 'Ignore list has been updated!')
321
+						else:
322
+							Commands.error(nick, 'Invalid option')
323
+					else:
324
+						Commands.error(nick, 'Invalid option', 'Must be a + or - prefixing the ident.')
325
+			elif args[0] == '.raw' and len(args) >= 2:
326
+				data = msg[5:]
327
+				Commands.raw(data)
328
+
329
+
330
+	def nick_in_use():
331
+		config.ident.nickname = 'scroll' + str(functions.random_int(10,99))
332
+		Commands.nick(config.ident.nickname)
333
+
334
+	def handle(data):
335
+		args = data.split()
336
+		if args[0] == 'PING':
337
+			Commands.raw('PONG ' + args[1][1:])
338
+		elif args[1] == '001':
339
+			Events.connect()
340
+		elif args[1] == '433':
341
+			Events.nick_in_use()
342
+		elif args[1] == 'KICK' and len(args) >= 4:
343
+			chan   = args[2]
344
+			kicked = args[3]
345
+			Events.kick(chan, kicked)
346
+		elif args[1] == 'PRIVMSG' and len(args) >= 4:
347
+			ident = args[0][1:]
348
+			nick  = args[0].split('!')[0][1:]
349
+			chan  = args[2]
350
+			msg   = ' '.join(args[3:])[1:]
351
+			if chan == config.ident.nickname:
352
+				Events.private(ident, nick, msg)
353
+			elif chan in (config.connection.channel,'#scroll'):
354
+				Events.message(ident, nick, chan, msg)
355
+
356
+Bot = IRC()

+ 1
- 0
scroll/data/.gitignore View File

@@ -0,0 +1 @@
1
+*.db

BIN
scroll/data/DejaVuSansMono.ttf View File


+ 4
- 0
scroll/data/ascii/.gitignore View File

@@ -0,0 +1,4 @@
1
+# Ignore everything in this directory
2
+*
3
+# Except this file
4
+!.gitignore

+ 4
- 0
scroll/data/logs/.gitignore View File

@@ -0,0 +1,4 @@
1
+# Ignore everything in this directory
2
+*
3
+# Except this file
4
+!.gitignore

+ 25
- 0
scroll/scroll.py View File

@@ -0,0 +1,25 @@
1
+#!/usr/bin/env python
2
+# Scroll IRC Bot
3
+# Developed by acidvegas in Python
4
+# https://git.acid.vegas/scroll
5
+# scroll.py
6
+
7
+import os
8
+import sys
9
+
10
+sys.dont_write_bytecode = True
11
+os.chdir(sys.path[0] or '.')
12
+sys.path += ('core',)
13
+
14
+import debug
15
+
16
+debug.setup_logger()
17
+debug.info()
18
+if not debug.check_version(3):
19
+	debug.error_exit('Python 3 is required!')
20
+if debug.check_privileges():
21
+	debug.error_exit('Do not run as admin/root!')
22
+import database
23
+database.check()
24
+import irc
25
+irc.Bot.connect()

Loading…
Cancel
Save