scroll

- irc bot to play ascii art
git clone git://git.acid.vegas/scroll.git
Log | Files | Refs | Archive | README | LICENSE

commit ac397afd833243d4e650510a8e9c71ced1a5ea5d
Author: acidvegas <acid.vegas@acid.vegas>
Date: Fri, 2 Jun 2023 01:43:45 -0400

Initial commit

Diffstat:
A.screens/preview.png | 0
ALICENSE | 16++++++++++++++++
AREADME.md | 27+++++++++++++++++++++++++++
Ascroll.py | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

4 files changed, 328 insertions(+), 0 deletions(-)

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