diff --git a/.screens/preview.png b/.screens/preview.png
Binary files differ.
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2021, acidvegas <acid.vegas@acid.vegas>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+\ No newline at end of file
diff --git a/README.md b/README.md
@@ -0,0 +1,26 @@
+# scroll
+
+Scroll is full-featured IRC bot that carries a **PENIS PUMP** & will brighten up all the mundane chats in your lame IRC channels with some colorful IRC artwork! Designed to be extremely stable, this bot is sure to stay rock hard & handle itself quite well!
+
+All of the IRC art is loaded directly from the [@ircart/ircart](https://github.com/ircart/ircart) repository.
+
+## Dependencies
+* [python](https://www.python.org/)
+* [chardet](https://pypi.org/project/chardet/) *(`pip install chardet`)*
+
+## Commands
+| Command | Description |
+| --------------------------------- | ---------------------------------------------------------- |
+| `@scroll` | information about scroll |
+| `.ascii <name>` | play the \<name> art file |
+| `.ascii dirs` | list of ascii directories |
+| `.ascii list` | list of ascii filenames |
+| `.ascii random [dir]` | play random art, optionally from the [dir] directory only |
+| `.ascii search <query>` | search for art diles that match \<query> |
+| `.ascii stop` | stop playing art |
+
+## Mirrors
+- [acid.vegas](https://git.acid.vegas/scroll)
+- [GitHub](https://github.com/ircart/scroll)
+- [GitLab](https://gitlab.com/ircart/scroll)
+- [SuperNETs](https://git.supernets.org/ircart/scroll)
+\ No newline at end of file
diff --git a/scroll.py b/scroll.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python
+# Scroll IRC Art Bot - Developed by acidvegas in Python (https://git.acid.vegas/scroll)
+
+import asyncio
+import random
+import ssl
+import time
+import urllib.request
+
+class connection:
+ server = 'irc.network.com'
+ port = 6697
+ ipv6 = False
+ ssl = True
+ vhost = None
+ channel = '#chats'
+ key = None
+ modes = None
+
+class identity:
+ nickname = 'scroll'
+ username = 'scroll'
+ realname = 'git.acid.vegas/scroll'
+ nickserv = None
+
+class throttle:
+ flood = 3 # delay between each command
+ max_lines = 300 # maximum number of lines in art file to be played outside of #scroll
+ message = 0.05 # delay between each line sent
+ results = 10 # maximum number of results returned from search
+
+# Formatting Control Characters / Color Codes
+bold = '\x02'
+italic = '\x1D'
+underline = '\x1F'
+reverse = '\x16'
+reset = '\x0f'
+white = '00'
+black = '01'
+blue = '02'
+green = '03'
+red = '04'
+brown = '05'
+purple = '06'
+orange = '07'
+yellow = '08'
+light_green = '09'
+cyan = '10'
+light_cyan = '11'
+light_blue = '12'
+pink = '13'
+grey = '14'
+light_grey = '15'
+
+def color(msg, foreground, background=None):
+ return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}'
+
+def debug(data):
+ print('{0} | [~] - {1}'.format(time.strftime('%I:%M:%S'), data))
+
+def error(data, reason=None):
+ print('{0} | [!] - {1} ({2})'.format(time.strftime('%I:%M:%S'), data, str(reason))) if reason else print('{0} | [!] - {1}'.format(time.strftime('%I:%M:%S'), data))
+
+def ssl_ctx():
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ return ctx
+
+class Bot():
+ def __init__(self):
+ self.db = dict()
+ self.last = 0
+ self.loops = dict()
+ self.playing = False
+ self.slow = False
+ self.reader = None
+ self.writer = None
+
+ async def raw(self, data):
+ self.writer.write(data[:510].encode('utf-8') + b'\r\n')
+ await self.writer.drain()
+
+ async def action(self, chan, msg):
+ await self.sendmsg(chan, f'\x01ACTION {msg}\x01')
+
+ async def sendmsg(self, target, msg):
+ await self.raw(f'PRIVMSG {target} :{msg}')
+
+ async def irc_error(self, chan, msg, reason=None):
+ await self.sendmsg(chan, '[{0}] {1} {2}'.format(color('ERROR', red), msg, color(f'({reason})', grey))) if reason else await self.sendmsg(chan, '[{0}] {1}'.format(color('ERROR', red), msg))
+
+ async def connect(self):
+ while True:
+ try:
+ options = {
+ 'host' : connection.server,
+ 'port' : connection.port,
+ 'limit' : 1024,
+ 'ssl' : ssl_ctx() if connection.ssl else None,
+ 'family' : 10 if connection.ipv6 else 2,
+ 'local_addr' : connection.vhost
+ }
+ self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15)
+ await self.raw(f'USER {identity.username} 0 * :{identity.realname}')
+ await self.raw('NICK ' + identity.nickname)
+ except Exception as ex:
+ error('failed to connect to ' + connection.server, ex)
+ else:
+ await self.listen()
+ finally:
+ self.loops = dict()
+ self.playing = False
+ self.slow = False
+ await asyncio.sleep(30)
+
+ async def sync(self):
+ try:
+ cache = self.db
+ self.db = dict()
+ ascii = urllib.request.urlopen('https://raw.githubusercontent.com/ircart/ircart/master/ircart/.list').readlines()
+ for item in ascii:
+ item = item.decode(chardet.detect(item)['encoding']).replace('\n','').replace('\r','')
+ if '/' in item:
+ dir = item.split('/')[0]
+ name = item.split('/')[1]
+ self.db[dir] = self.db[dir]+[name,] if dir in self.db else [name,]
+ else:
+ self.db['root'] = self.db['root']+[item,] if 'root' in self.db else [item,]
+ except Exception as ex:
+ try:
+ await self.irc_error(connection.channel, 'failed to sync database', ex)
+ except:
+ error(connection.channel, 'failed to sync database', ex)
+ self.db = cache
+
+ async def play(self, chan, name):
+ try:
+ ascii = urllib.request.urlopen(f'https://raw.githubusercontent.com/ircart/ircart/master/ircart/{name}.txt', timeout=10)
+ if ascii.getcode() == 200:
+ ascii = ascii.readlines()
+ if len(ascii) > throttle.max_lines and chan != '#scroll':
+ await self.irc_error(chan, 'file is too big', 'take it to #scroll')
+ else:
+ await self.action(chan, 'the ascii gods have chosen... ' + color(name, cyan))
+ for line in ascii:
+ await self.sendmsg(chan, line.decode(chardet.detect(line)['encoding']).replace('\n','').replace('\r','') + reset)
+ await asyncio.sleep(throttle.message)
+ else:
+ await self.irc_error(chan, 'invalid name', name)
+ except Exception as ex:
+ try:
+ await self.irc_error(chan, 'error in play function', ex)
+ except:
+ error('error in play function', ex)
+ finally:
+ self.playing = False
+
+ async def listen(self):
+ while True:
+ try:
+ if self.reader.at_eof():
+ break
+ data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 200)
+ line = data.decode('utf-8').strip()
+ args = line.split()
+ debug(line)
+ if line.startswith('ERROR :Closing Link:'):
+ raise Exception('Connection has closed.')
+ elif args[0] == 'PING':
+ await self.raw('PONG '+args[1][1:])
+ elif args[1] == '001':
+ if connection.modes:
+ await self.raw(f'MODE {identity.nickname} +{connection.modes}')
+ if identity.nickserv:
+ await self.sendmsg('NickServ', f'IDENTIFY {identity.nickname} {identity.nickserv}')
+ await self.raw(f'JOIN {connection.channel} {connection.key}') if connection.key else await self.raw('JOIN ' + connection.channel)
+ await self.raw('JOIN #scroll')
+ await self.sync()
+ elif args[1] == '433':
+ error('The bot is already running or nick is in use.')
+ elif args[1] == 'INVITE' and len(args) == 4:
+ invited = args[2]
+ chan = args[3][1:]
+ if invited == identity.nickname and chan in (connection.channel, '#scroll'):
+ await self.raw(f'JOIN {connection.channel} {connection.key}') if connection.key else await self.raw('JOIN ' + connection.channel)
+ elif args[1] == 'KICK' and len(args) >= 4:
+ chan = args[2]
+ kicked = args[3]
+ if kicked == identity.nickname and chan in (connection.channel,'#scroll'):
+ await asyncio.sleep(3)
+ await self.raw(f'JOIN {connection.channel} {connection.key}') if connection.key else await self.raw('JOIN ' + connection.channel)
+ elif args[1] == 'PRIVMSG' and len(args) >= 4:
+ nick = args[0].split('!')[0][1:]
+ chan = args[2]
+ msg = ' '.join(args[3:])[1:]
+ if chan in (connection.channel, '#scroll'):
+ args = msg.split()
+ if msg == '@cancer':
+ await self.sendmsg(chan, bold + 'CANCER IRC Bot - Developed by acidvegas in Python - https://git.acid.vegas/cancer')
+ elif args[0] == '.ascii':
+ if msg == '.ascii stop' and self.playing:
+ if chan in self.loops:
+ self.loops[chan].cancel()
+ elif time.time() - self.last < throttle.flood:
+ if not self.slow:
+ if not self.playing:
+ await self.irc_error(chan, 'slow down nerd')
+ self.slow = True
+ elif len(args) >= 2 and not self.playing:
+ self.slow = False
+ if msg == '.ascii dirs':
+ for dir in self.db:
+ await self.sendmsg(chan, '[{0}] {1}{2}'.format(color(str(list(self.db).index(dir)+1).zfill(2), pink), dir.ljust(10), color('('+str(len(self.db[dir]))+')', grey)))
+ await asyncio.sleep(throttle.message)
+ elif msg == '.ascii list':
+ await self.sendmsg(chan, underline + color('https://raw.githubusercontent.com/ircart/ircart/master/ircart/.list', light_blue))
+ elif msg == '.ascii random':
+ self.playing = True
+ dir = random.choice(list(self.db))
+ ascii = random.choice(self.db[dir]) if dir == 'root' else dir+'/'+random.choice(self.db[dir])
+ self.loops[chan] = asyncio.create_task(self.play(chan, ascii))
+ elif args[1] == 'random' and len(args) == 3:
+ dir = args[2]
+ if dir in self.db:
+ self.playing = True
+ ascii = random.choice(self.db[dir])
+ self.loops[chan] = asyncio.create_task(self.play(chan, dir+'/'+ascii))
+ else:
+ await self.irc_error(chan, 'invalid directory name', dir)
+ elif args[1] == 'search' and len(args) == 3:
+ query = args[2]
+ results = list()
+ for dir in self.db:
+ for ascii in self.db[dir]:
+ if query in ascii:
+ results.append({'dir':dir,'name':ascii})
+ if results:
+ for item in results[:throttle.results]:
+ if item['dir'] == 'root':
+ await self.sendmsg(chan, '[{0}] {1}'.format(color(str(results.index(item)+1).zfill(2), pink), item['name']))
+ else:
+ await self.sendmsg(chan, '[{0}] {1} {2}'.format(color(str(results.index(item)+1).zfill(2), pink), item['name'], color('('+item['dir']+')', grey)))
+ await asyncio.sleep(throttle.message)
+ else:
+ await self.irc_error(chan, 'no results found', query)
+ elif len(args) == 2:
+ option = args[1]
+ if [x for x in ('..','?','%','\\') if x in option]:
+ await self.irc_error(chan, 'nice try nerd')
+ elif option == 'random':
+ self.playing = True
+ self.loops[chan] = asyncio.create_task(self.play(chan, random.choice(self.db)))
+ else:
+ ascii = [dir+'/'+option for dir in self.db if option in self.db[dir]][0]
+ if ascii:
+ if ascii.startswith('root/'):
+ ascii = ascii.split('/')[1]
+ self.playing = True
+ self.loops[chan] = asyncio.create_task(self.play(chan, ascii))
+ else:
+ await self.irc_error(chan, 'no results found', option)
+ except (UnicodeDecodeError, UnicodeEncodeError):
+ pass
+ except Exception as ex:
+ error('fatal error occured', ex)
+ break
+ finally:
+ self.last = time.time()
+
+# Main
+print('#'*56)
+print('#{:^54}#'.format(''))
+print('#{:^54}#'.format('Scroll IRC Art Bot'))
+print('#{:^54}#'.format('Developed by acidvegas in Python'))
+print('#{:^54}#'.format('https://git.acid.vegas/scroll'))
+print('#{:^54}#'.format(''))
+print('#'*56)
+try:
+ import chardet
+except ImportError:
+ raise SystemExit('missing required \'chardet\' library (https://pypi.org/project/chardet/)')
+else:
+ asyncio.run(Bot().connect())
+\ No newline at end of file
| | | |