commit ac397afd833243d4e650510a8e9c71ced1a5ea5d Author: acidvegas Date: Fri Jun 2 01:43:45 2023 -0400 Initial commit diff --git a/.screens/preview.png b/.screens/preview.png new file mode 100644 index 0000000..0321f8f Binary files /dev/null and b/.screens/preview.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..881204a --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021, acidvegas + +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 new file mode 100644 index 0000000..9187135 --- /dev/null +++ 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 ` | play the \ 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 ` | search for art diles that match \ | +| `.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 new file mode 100644 index 0000000..3afb8d9 --- /dev/null +++ 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