mirror of
https://github.com/oddluck/limnoria-plugins.git
synced 2025-04-25 12:31:07 -05:00
1824 lines
69 KiB
Python
1824 lines
69 KiB
Python
###
|
||
# Copyright (c) 2020, oddluck <oddluck@riseup.net>
|
||
# All rights reserved.
|
||
#
|
||
# Redistribution and use in source and binary forms, with or without
|
||
# modification, are permitted provided that the following conditions are met:
|
||
#
|
||
# * Redistributions of source code must retain the above copyright notice,
|
||
# this list of conditions, and the following disclaimer.
|
||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||
# this list of conditions, and the following disclaimer in the
|
||
# documentation and/or other materials provided with the distribution.
|
||
# * Neither the name of the author of this software nor the name of
|
||
# contributors to this software may be used to endorse or promote products
|
||
# derived from this software without specific prior written consent.
|
||
#
|
||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
# POSSIBILITY OF SUCH DAMAGE.
|
||
###
|
||
|
||
import supybot.ansi as ansi
|
||
import supybot.utils as utils
|
||
from supybot.commands import *
|
||
import supybot.plugins as plugins
|
||
import supybot.ircutils as ircutils
|
||
import supybot.ircdb as ircdb
|
||
import supybot.callbacks as callbacks
|
||
import supybot.ircmsgs as ircmsgs
|
||
import supybot.log as log
|
||
import os
|
||
import requests
|
||
from PIL import Image, ImageOps, ImageFont, ImageDraw, ImageEnhance
|
||
import numpy as np
|
||
import sys, math
|
||
import re
|
||
import asyncio
|
||
import pexpect
|
||
import time
|
||
import random
|
||
import pyimgur
|
||
from bs4 import BeautifulSoup
|
||
import json
|
||
from .colors import (
|
||
rgbColors,
|
||
colors16,
|
||
colors83,
|
||
colors99,
|
||
ansi16,
|
||
ansi83,
|
||
ansi99,
|
||
x16colors,
|
||
)
|
||
|
||
try:
|
||
from supybot.i18n import PluginInternationalization
|
||
|
||
_ = PluginInternationalization("TextArt")
|
||
except ImportError:
|
||
# Placeholder that allows to run the plugin on a bot
|
||
# without the i18n module
|
||
_ = lambda x: x
|
||
|
||
|
||
class TextArt(callbacks.Plugin):
|
||
"""TextArt: Make Text Art"""
|
||
|
||
threaded = True
|
||
|
||
def __init__(self, irc):
|
||
self.__parent = super(TextArt, self)
|
||
self.__parent.__init__(irc)
|
||
self.colors = 99
|
||
self.stopped = {}
|
||
self.old_color = None
|
||
self.source_colors = 0
|
||
self.agents = self.registryValue("userAgents")
|
||
self.matches = {}
|
||
|
||
def doPrivmsg(self, irc, msg):
|
||
channel = msg.args[0]
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
self.stopped.setdefault(channel, None)
|
||
if msg.args[1].lower().strip()[1:] == "cq":
|
||
self.stopped[channel] = True
|
||
|
||
def doPaste(self, description, paste):
|
||
try:
|
||
description = description.split("/")[-1]
|
||
apikey = self.registryValue("pasteAPI")
|
||
payload = {"description": description, "sections": [{"contents": paste}]}
|
||
headers = {"X-Auth-Token": apikey}
|
||
post_response = requests.post(
|
||
url="https://api.paste.ee/v1/pastes", json=payload, headers=headers
|
||
)
|
||
response = json.loads(post_response.content)
|
||
return response["link"].replace("/p/", "/r/")
|
||
except:
|
||
return (
|
||
"Error. Did you set a valid Paste.ee API Key?"
|
||
" https://paste.ee/account/api"
|
||
)
|
||
|
||
def renderImage(self, text, size=18, defaultBg=1, defaultFg=0):
|
||
try:
|
||
if utf8 and not isinstance(text, unicode):
|
||
text = text.decode("utf-8")
|
||
except:
|
||
pass
|
||
text = text.replace("\t", " ")
|
||
self.strip_colors_regex = re.compile(
|
||
"(\x03([0-9]{1,2})(,[0-9]{1,2})?)|[\x0f\x02\x1f\x03\x16]"
|
||
).sub
|
||
path = os.path.dirname(os.path.abspath(__file__))
|
||
defaultFont = "{0}/DejaVu.ttf".format(path)
|
||
|
||
def strip_colors(string):
|
||
return self.strip_colors_regex("", string)
|
||
|
||
_colorRegex = re.compile("(([0-9]{1,2})(,([0-9]{1,2}))?)")
|
||
IGNORE_CHRS = ("\x16", "\x1f", "\x02", "\x03", "\x0f")
|
||
lineLens = [len(line) for line in strip_colors(text).splitlines()]
|
||
maxWidth, height = max(lineLens), len(lineLens)
|
||
font = ImageFont.truetype(defaultFont, size)
|
||
fontX = 10
|
||
fontY = 20
|
||
imageX, imageY = maxWidth * fontX, height * fontY
|
||
image = Image.new("RGB", (imageX, imageY), rgbColors[defaultBg])
|
||
draw = ImageDraw.Draw(image)
|
||
dtext, drect, match, x, y, fg, bg = (
|
||
draw.text,
|
||
draw.rectangle,
|
||
_colorRegex.match,
|
||
0,
|
||
0,
|
||
defaultFg,
|
||
defaultBg,
|
||
)
|
||
for text in text.split("\n"):
|
||
ll, i = len(text), 0
|
||
while i < ll:
|
||
chr = text[i]
|
||
if chr == "\x03":
|
||
m = match(text[i + 1 : i + 6])
|
||
if m:
|
||
i += len(m.group(1))
|
||
fg = int(m.group(2))
|
||
if m.group(4) is not None:
|
||
bg = int(m.group(4))
|
||
else:
|
||
bg, fg = defaultBg, defaultFg
|
||
elif chr == "\x0f":
|
||
fg, bg = defaultFg, defaultBg
|
||
elif chr not in IGNORE_CHRS:
|
||
if bg != defaultBg: # bg is not white, render it
|
||
drect((x, y, x + fontX, y + fontY), fill=rgbColors[bg])
|
||
if bg != fg: # text will show, render it. this saves a lot of time!
|
||
dtext((x, y), chr, font=font, fill=rgbColors[fg])
|
||
x += fontX
|
||
i += 1
|
||
y += fontY
|
||
fg, bg, x = defaultFg, defaultBg, 0
|
||
return image
|
||
|
||
def getColor(self, pixel, speed):
|
||
pixel = tuple(pixel)
|
||
try:
|
||
return self.matches[pixel]
|
||
except KeyError:
|
||
if self.colors == 16:
|
||
colors = list(colors16.keys())
|
||
elif self.colors == 99:
|
||
colors = list(colors99.keys())
|
||
else:
|
||
colors = list(colors83.keys())
|
||
closest_colors = sorted(
|
||
colors,
|
||
key=lambda color: self.distance(color, self.rgb2lab(pixel), speed),
|
||
)
|
||
closest_color = closest_colors[0]
|
||
if self.colors == 16:
|
||
self.matches[pixel] = colors16[closest_color]
|
||
elif self.colors == 99:
|
||
self.matches[pixel] = colors99[closest_color]
|
||
else:
|
||
self.matches[pixel] = colors83[closest_color]
|
||
self.source_colors += 1
|
||
return self.matches[pixel]
|
||
|
||
def rgb2lab(self, inputColor):
|
||
try:
|
||
return self.labmatches[inputColor]
|
||
except:
|
||
num = 0
|
||
RGB = [0, 0, 0]
|
||
for value in inputColor:
|
||
value = float(value) / 255
|
||
if value > 0.04045:
|
||
value = ((value + 0.055) / 1.055) ** 2.4
|
||
else:
|
||
value = value / 12.92
|
||
RGB[num] = value * 100
|
||
num = num + 1
|
||
XYZ = [
|
||
0,
|
||
0,
|
||
0,
|
||
]
|
||
X = RGB[0] * 0.4124 + RGB[1] * 0.3576 + RGB[2] * 0.1805
|
||
Y = RGB[0] * 0.2126 + RGB[1] * 0.7152 + RGB[2] * 0.0722
|
||
Z = RGB[0] * 0.0193 + RGB[1] * 0.1192 + RGB[2] * 0.9505
|
||
XYZ[0] = round(X, 4)
|
||
XYZ[1] = round(Y, 4)
|
||
XYZ[2] = round(Z, 4)
|
||
XYZ[0] = (
|
||
float(XYZ[0]) / 95.047
|
||
) # ref_X = 95.047 Observer= 2°, Illuminant= D65
|
||
XYZ[1] = float(XYZ[1]) / 100.0 # ref_Y = 100.000
|
||
XYZ[2] = float(XYZ[2]) / 108.883 # ref_Z = 108.883
|
||
num = 0
|
||
for value in XYZ:
|
||
if value > 0.008856:
|
||
value = value ** (0.3333333333333333)
|
||
else:
|
||
value = (7.787 * value) + (16 / 116)
|
||
XYZ[num] = value
|
||
num = num + 1
|
||
Lab = [0, 0, 0]
|
||
L = (116 * XYZ[1]) - 16
|
||
a = 500 * (XYZ[0] - XYZ[1])
|
||
b = 200 * (XYZ[1] - XYZ[2])
|
||
Lab[0] = round(L, 4)
|
||
Lab[1] = round(a, 4)
|
||
Lab[2] = round(b, 4)
|
||
self.labmatches[inputColor] = Lab
|
||
return self.labmatches[inputColor]
|
||
|
||
def ciede2000(self, lab1, lab2):
|
||
""" CIEDE2000 color difference formula. https://peteroupc.github.io/colorgen.html"""
|
||
dl = lab2[0] - lab1[0]
|
||
hl = lab1[0] + dl * 0.5
|
||
sqb1 = lab1[2] * lab1[2]
|
||
sqb2 = lab2[2] * lab2[2]
|
||
c1 = math.sqrt(lab1[1] * lab1[1] + sqb1)
|
||
c2 = math.sqrt(lab2[1] * lab2[1] + sqb2)
|
||
hc7 = math.pow((c1 + c2) * 0.5, 7)
|
||
trc = math.sqrt(hc7 / (hc7 + 6103515625))
|
||
t2 = 1.5 - trc * 0.5
|
||
ap1 = lab1[1] * t2
|
||
ap2 = lab2[1] * t2
|
||
c1 = math.sqrt(ap1 * ap1 + sqb1)
|
||
c2 = math.sqrt(ap2 * ap2 + sqb2)
|
||
dc = c2 - c1
|
||
hc = c1 + dc * 0.5
|
||
hc7 = math.pow(hc, 7)
|
||
trc = math.sqrt(hc7 / (hc7 + 6103515625))
|
||
h1 = math.atan2(lab1[2], ap1)
|
||
if h1 < 0:
|
||
h1 = h1 + math.pi * 2
|
||
h2 = math.atan2(lab2[2], ap2)
|
||
if h2 < 0:
|
||
h2 = h2 + math.pi * 2
|
||
hdiff = h2 - h1
|
||
hh = h1 + h2
|
||
if abs(hdiff) > math.pi:
|
||
hh = hh + math.pi * 2
|
||
if h2 <= h1:
|
||
hdiff = hdiff + math.pi * 2
|
||
else:
|
||
hdiff = hdiff - math.pi * 2
|
||
hh = hh * 0.5
|
||
t2 = 1 - 0.17 * math.cos(hh - math.pi / 6) + 0.24 * math.cos(hh * 2)
|
||
t2 = t2 + 0.32 * math.cos(hh * 3 + math.pi / 30)
|
||
t2 = t2 - 0.2 * math.cos(hh * 4 - math.pi * 63 / 180)
|
||
dh = 2 * math.sqrt(c1 * c2) * math.sin(hdiff * 0.5)
|
||
sqhl = (hl - 50) * (hl - 50)
|
||
fl = dl / (1 + (0.015 * sqhl / math.sqrt(20 + sqhl)))
|
||
fc = dc / (hc * 0.045 + 1)
|
||
fh = dh / (t2 * hc * 0.015 + 1)
|
||
dt = 30 * math.exp(
|
||
-math.pow(36 * hh - 55 * math.pi, 2) / (25 * math.pi * math.pi)
|
||
)
|
||
r = -2 * trc * math.sin(2 * dt * math.pi / 180)
|
||
de = math.sqrt(fl * fl + fc * fc + fh * fh + r * fc * fh)
|
||
dep = 1.43 * de ** 0.70
|
||
return dep
|
||
|
||
def distance(self, c1, c2, speed):
|
||
if speed == "fast":
|
||
delta_e = math.sqrt(
|
||
(c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2 + (c1[2] - c2[2]) ** 2
|
||
)
|
||
elif speed == "slow":
|
||
delta_e = self.ciede2000(c1, c2)
|
||
return delta_e
|
||
|
||
def process_ansi(self, ansi):
|
||
if self.colors == 16:
|
||
colors = ansi16
|
||
elif self.colors == 99:
|
||
colors = ansi99
|
||
else:
|
||
colors = ansi83
|
||
x16color1 = None
|
||
x16color2 = None
|
||
x256color1 = None
|
||
x256color2 = None
|
||
effect = None
|
||
ansi = ansi.lower().strip("\x1b[").strip("m").split(";")
|
||
if len(ansi) > 1:
|
||
i = 0
|
||
while i < len(ansi):
|
||
if i >= len(ansi):
|
||
break
|
||
elif ansi[i] == "0":
|
||
effect = 0
|
||
i += 1
|
||
continue
|
||
elif ansi[i] == "1":
|
||
effect = 1
|
||
i += 1
|
||
continue
|
||
elif ansi[i] == "4":
|
||
effect = 4
|
||
i += 1
|
||
continue
|
||
elif ansi[i] == "2":
|
||
effect = 2
|
||
i += 1
|
||
continue
|
||
elif int(ansi[i]) > 29 and int(ansi[i]) < 38:
|
||
if effect == 1 or ansi[-1] == "1":
|
||
x16color1 = x16colors["{0};1".format(ansi[i])]
|
||
effect = None
|
||
i += 1
|
||
continue
|
||
else:
|
||
x16color1 = x16colors[ansi[i]]
|
||
i += 1
|
||
continue
|
||
elif int(ansi[i]) > 39 and int(ansi[i]) < 48:
|
||
if effect == 1 or ansi[-1] == "1":
|
||
x16color2 = x16colors["{0};1".format(ansi[i])]
|
||
effect = None
|
||
i += 1
|
||
continue
|
||
else:
|
||
x16color2 = x16colors[ansi[i]]
|
||
i += 1
|
||
continue
|
||
elif ansi[i] == "38":
|
||
x256color1 = colors[int(ansi[i + 2])]
|
||
i += 3
|
||
continue
|
||
elif ansi[i] == "48":
|
||
x256color2 = colors[int(ansi[i + 2])]
|
||
i += 3
|
||
continue
|
||
else:
|
||
i += 1
|
||
continue
|
||
if x16color1 and x16color2:
|
||
color = "\x03{0},{1}".format(x16color1, x16color2)
|
||
elif x256color1 and x256color2:
|
||
color = "\x03{0},{1}".format(x256color1, x256color2)
|
||
elif x16color1:
|
||
color = "\x03{0}".format(x16color1)
|
||
elif x16color2:
|
||
color = "\x0399,{0}".format(x16color2)
|
||
elif x256color1:
|
||
color = "\x03{0}".format(x256color1)
|
||
elif x256color2:
|
||
color = "\x0399,{0}".format(x256color2)
|
||
else:
|
||
color = ""
|
||
if effect == 1:
|
||
color += "\x02"
|
||
if effect == 4:
|
||
color += "\x1F"
|
||
elif len(ansi[0]) > 0:
|
||
if ansi[0] == "0":
|
||
color = "\x0F"
|
||
elif ansi[0] == "1" or ansi[0] == "2":
|
||
color = "\x02"
|
||
elif ansi[0] == "4":
|
||
color = "\x1F"
|
||
elif int(ansi[0]) > 29 and int(ansi[0]) < 38:
|
||
color = "\x03{0}".format(x16colors[ansi[0]])
|
||
elif int(ansi[0]) > 39 and int(ansi[0]) < 48:
|
||
color = "\x0399,{0}".format(x16colors[ansi[0]])
|
||
elif ansi[0][-1] == "c":
|
||
color = " " * int(ansi[0][:-1])
|
||
else:
|
||
color = ""
|
||
else:
|
||
color = ""
|
||
if color != self.old_color:
|
||
self.old_color = color
|
||
return color
|
||
else:
|
||
return ""
|
||
|
||
def ansi2irc(self, output):
|
||
output = output.replace("\x1b(B\x1b[m", "\x1b[0m")
|
||
output = output.replace("\x1b\x1b", "\x1b")
|
||
output = re.sub(
|
||
"\x1B\[[0-?]*[ -/]*[@-~]", lambda m: self.process_ansi(m.group(0)), output
|
||
)
|
||
output = re.sub("\x0399,(\d\d)\x03(\d\d)", "\x03\g<2>,\g<1>", output)
|
||
output = output.replace("\x0F\x03", "\x03")
|
||
return output
|
||
|
||
def png(self, irc, msg, args, optlist, url):
|
||
"""[--bg] [--fg] <url>
|
||
Generate PNG from text file
|
||
"""
|
||
optlist = dict(optlist)
|
||
if "bg" in optlist:
|
||
bg = optlist.get("bg")
|
||
else:
|
||
bg = 1
|
||
if "fg" in optlist:
|
||
fg = optlist.get("fg")
|
||
else:
|
||
fg = 0
|
||
if url.startswith("https://paste.ee/p/"):
|
||
url = re.sub("https://paste.ee/p/", "https://paste.ee/r/", url)
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
try:
|
||
r = requests.get(url, stream=True, headers=header, timeout=10)
|
||
r.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for png: {0}".format(e))
|
||
return
|
||
if "text/plain" in r.headers["content-type"] or url.startswith(
|
||
"https://paste.ee/r/"
|
||
):
|
||
try:
|
||
file = r.content.decode()
|
||
except:
|
||
file = r.content.decode("cp437")
|
||
else:
|
||
irc.reply("Invalid file type.", private=False, notice=False)
|
||
return
|
||
file = re.sub("(\x03(\d+).*)\x03,", "\g<1>\x03\g<2>,", file).replace(
|
||
"\r\n", "\n"
|
||
)
|
||
im = self.renderImage(file, 18, bg, fg)
|
||
path = os.path.dirname(os.path.abspath(__file__))
|
||
filepath = "{0}/tmp/tldr.png".format(path)
|
||
im.save(filepath, "PNG")
|
||
CLIENT_ID = self.registryValue("imgurAPI")
|
||
imgur = pyimgur.Imgur(CLIENT_ID)
|
||
uploaded_image = imgur.upload_image(filepath, title=url)
|
||
irc.reply(uploaded_image.link, noLengthCheck=True, private=False, notice=False)
|
||
|
||
png = wrap(png, [getopts({"bg": "int", "fg": "int"}), "text"])
|
||
|
||
async def reply(self, irc, output, channel, delay):
|
||
self.stopped.setdefault(channel, None)
|
||
for line in output:
|
||
if self.stopped[channel]:
|
||
return
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
irc.sendMsg(ircmsgs.privmsg(channel, line))
|
||
await asyncio.sleep(delay)
|
||
|
||
def artii(self, irc, msg, args, channel, optlist, text):
|
||
"""[<channel>] [--font <font>] [--color <color1,color2>] [<text>]
|
||
Text to ASCII figlet fonts using the artii API
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
if len(text) > self.registryValue("maxLength", msg.channel):
|
||
return
|
||
if len(text.split(" ")) > self.registryValue("maxWords", msg.channel):
|
||
return
|
||
elif len(text.split("|")) > self.registryValue("maxWords", msg.channel):
|
||
return
|
||
optlist = dict(optlist)
|
||
font = None
|
||
words = []
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if text:
|
||
text = text.strip()
|
||
if "|" in text:
|
||
words = text.split("|")
|
||
if "color" in optlist:
|
||
color = optlist.get("color")
|
||
if "," in color:
|
||
color = color.split(",")
|
||
color1 = color[0].strip()
|
||
color2 = color[1].strip()
|
||
else:
|
||
color1 = color
|
||
color2 = None
|
||
else:
|
||
color1 = None
|
||
color2 = None
|
||
if "font" in optlist:
|
||
font = optlist.get("font")
|
||
if words:
|
||
for word in words:
|
||
if word.strip():
|
||
try:
|
||
data = requests.get(
|
||
"https://artii.herokuapp.com/make?text={0}&font={1}"
|
||
.format(word.strip(), font),
|
||
timeout=10,
|
||
)
|
||
data.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug(
|
||
"TextArt: error retrieving data for artii: {0}".format(
|
||
e
|
||
)
|
||
)
|
||
return
|
||
output = []
|
||
for line in data.content.decode().splitlines():
|
||
line = ircutils.mircColor(line, color1, color2)
|
||
output.append(line)
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
else:
|
||
try:
|
||
data = requests.get(
|
||
"https://artii.herokuapp.com/make?text={0}&font={1}".format(
|
||
text, font
|
||
),
|
||
timeout=10,
|
||
)
|
||
data.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for artii: {0}".format(e))
|
||
return
|
||
output = []
|
||
for line in data.content.decode().splitlines():
|
||
line = ircutils.mircColor(line, color1, color2)
|
||
output.append(line)
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
elif "font" not in optlist:
|
||
if words:
|
||
for word in words:
|
||
if word.strip():
|
||
try:
|
||
data = requests.get(
|
||
"https://artii.herokuapp.com/make?text={0}&font=univers"
|
||
.format(word.strip()),
|
||
timeout=10,
|
||
)
|
||
data.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug(
|
||
"TextArt: error retrieving data for artii: {0}".format(
|
||
e
|
||
)
|
||
)
|
||
return
|
||
output = []
|
||
for line in data.content.decode().splitlines():
|
||
line = ircutils.mircColor(line, color1, color2)
|
||
output.append(line)
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
else:
|
||
try:
|
||
data = requests.get(
|
||
"https://artii.herokuapp.com/make?text={0}&font=univers".format(
|
||
text
|
||
),
|
||
timeout=10,
|
||
)
|
||
data.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for artii: {0}".format(e))
|
||
return
|
||
output = []
|
||
for line in data.content.decode().splitlines():
|
||
line = ircutils.mircColor(line, color1, color2)
|
||
output.append(line)
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
|
||
artii = wrap(
|
||
artii,
|
||
[
|
||
optional("channel"),
|
||
getopts({"font": "text", "color": "text", "delay": "float"}),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def fontlist(self, irc, msg, args):
|
||
"""
|
||
Get list of artii figlet fonts.
|
||
"""
|
||
try:
|
||
fontlist = requests.get(
|
||
"https://artii.herokuapp.com/fonts_list", timeout=10
|
||
)
|
||
fontlist.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("textArt: error retrieving data for fontlist: {0}".format(e))
|
||
return
|
||
response = sorted(fontlist.content.decode().split("\n"))
|
||
irc.reply(str(response).replace("'", "").replace("[", "").replace("]", ""))
|
||
|
||
fontlist = wrap(fontlist)
|
||
|
||
def img(self, irc, msg, args, channel, optlist, url):
|
||
"""[<#channel>] [--delay #.#] [--w <###>] [--s <#.#] [--16] [--99] [--83] [--ascii] [--block] [--1/2] [--chars <text>] [--ramp <text>] [--bg <0-98>] [--fg <0-98>] [--no-color] [--invert] <url>
|
||
Image to IRC art.
|
||
--w columns.
|
||
--s saturation (1.0).
|
||
--16 colors 0-15.
|
||
--99 colors 0-98.
|
||
--83 colors 16-98.
|
||
--ascii color ascii.
|
||
--block space block.
|
||
--1/2 for 1/2 block
|
||
--chars <TEXT> color text.
|
||
--ramp <TEXT> set ramp (".:-=+*#%@").
|
||
--bg <0-98> set bg.
|
||
--fg <0-99> set fg.
|
||
--no-color greyscale ascii.
|
||
--invert inverts ramp.
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
gscale = "\xa0"
|
||
if "16" in optlist:
|
||
self.colors = 16
|
||
elif "83" in optlist:
|
||
self.colors = 83
|
||
elif "99" in optlist:
|
||
self.colors = 99
|
||
else:
|
||
self.colors = self.registryValue("colors", msg.args[0])
|
||
if "fast" in optlist:
|
||
speed = "fast"
|
||
elif "slow" in optlist:
|
||
speed = "slow"
|
||
else:
|
||
speed = self.registryValue("speed", msg.args[0]).lower()
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if "quantize" in optlist:
|
||
quantize = True
|
||
elif "no-quantize" in optlist:
|
||
quantize = False
|
||
else:
|
||
quantize = self.registryValue("quantize", msg.args[0])
|
||
if "bg" in optlist:
|
||
bg = optlist.get("bg")
|
||
else:
|
||
bg = self.registryValue("bg", msg.args[0])
|
||
if "fg" in optlist:
|
||
fg = optlist.get("fg")
|
||
else:
|
||
fg = self.registryValue("fg", msg.args[0])
|
||
if "chars" in optlist:
|
||
type = "ascii"
|
||
gscale = optlist.get("chars")
|
||
elif "ramp" in optlist:
|
||
type = "ascii"
|
||
gscale = optlist.get("ramp")
|
||
elif "ascii" in optlist and bg == 0 or bg == 98:
|
||
type = "ascii"
|
||
gscale = (
|
||
"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:\"^`'."
|
||
)
|
||
elif "ascii" in optlist:
|
||
type = "ascii"
|
||
gscale = (
|
||
".'`^\":;Il!i><~+_-?][}{1)(|\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
|
||
)
|
||
elif "1/2" in optlist:
|
||
type = "1/2"
|
||
elif "block" in optlist:
|
||
type = "ascii"
|
||
gscale = "\xa0"
|
||
else:
|
||
type = self.registryValue("imgDefault", msg.args[0]).lower()
|
||
if "no-color" in optlist and "ramp" not in optlist and bg == 0 or bg == 98:
|
||
type = "no-color"
|
||
gscale = "@%#*+=-:. "
|
||
elif "no-color" in optlist and "ramp" not in optlist:
|
||
type = "no-color"
|
||
gscale = " .:-=+*#%@"
|
||
elif "no-color" in optlist and "chars" not in optlist:
|
||
type = "no-color"
|
||
if not gscale.strip():
|
||
gscale = "\xa0"
|
||
if "invert" in optlist and "chars" not in optlist and gscale != "\xa0":
|
||
gscale = gscale[::-1]
|
||
if "w" in optlist:
|
||
cols = optlist.get("w")
|
||
elif type == "ascii" or type == "no-color" or type == "block":
|
||
cols = self.registryValue("asciiWidth", msg.args[0])
|
||
else:
|
||
cols = self.registryValue("blockWidth", msg.args[0])
|
||
if "s" in optlist:
|
||
s = float(optlist.get("s"))
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
image_formats = ("image/png", "image/jpeg", "image/jpg", "image/gif")
|
||
try:
|
||
r = requests.get(url, stream=True, headers=header, timeout=10)
|
||
r.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for img: {0}".format(e))
|
||
return
|
||
if r.headers["content-type"] in image_formats and r.status_code == 200:
|
||
r.raw.decode_content = True
|
||
image = Image.open(r.raw)
|
||
else:
|
||
irc.reply("Error: Invalid file type.", private=False, notice=False)
|
||
return
|
||
# open image and convert to grayscale
|
||
start_time = time.time()
|
||
self.source_colors = 0
|
||
if image.mode == "RGBA":
|
||
if bg == 99:
|
||
newbg = 1
|
||
else:
|
||
newbg = bg
|
||
image = Image.alpha_composite(
|
||
Image.new("RGBA", image.size, rgbColors[newbg] + (255,)), image
|
||
)
|
||
if image.mode != "RGB":
|
||
image = image.convert("RGB")
|
||
try:
|
||
os.remove(filename)
|
||
except:
|
||
pass
|
||
# store dimensions
|
||
W, H = image.size[0], image.size[1]
|
||
# compute width of tile
|
||
w = W / cols
|
||
# compute tile height based on aspect ratio and scale
|
||
if type == "1/2":
|
||
scale = 1.0
|
||
else:
|
||
scale = 0.5
|
||
h = w / scale
|
||
# compute number of rows
|
||
rows = int(H / h)
|
||
if "resize" in optlist:
|
||
resize = optlist.get("resize")
|
||
else:
|
||
resize = self.registryValue("resize", msg.args[0])
|
||
if type != "no-color":
|
||
image2 = image.resize((cols, rows), resize)
|
||
if "s" in optlist:
|
||
image2 = ImageEnhance.Color(image2).enhance(s)
|
||
if quantize:
|
||
image2 = image2.quantize(dither=None)
|
||
image2 = image2.convert("RGB")
|
||
colormap = np.array(image2)
|
||
self.labmatches = {}
|
||
if not self.registryValue("cacheColors"):
|
||
self.matches = {}
|
||
# ascii image is a list of character strings
|
||
aimg = []
|
||
if type == "1/2":
|
||
k = 0
|
||
for j in range(0, rows - 1, 2):
|
||
# append an empty string
|
||
aimg.append("")
|
||
old_color1 = "99"
|
||
old_color2 = "99"
|
||
old_char = None
|
||
for i in range(cols):
|
||
color1 = "%02d" % self.getColor(colormap[j][i].tolist(), speed)
|
||
color2 = "%02d" % self.getColor(colormap[j + 1][i].tolist(), speed)
|
||
if color1 == color2:
|
||
gsval = " "
|
||
else:
|
||
gsval = "▀"
|
||
if color1 == old_color1 and color2 == old_color2:
|
||
aimg[k] += gsval
|
||
old_char = gsval
|
||
elif gsval == " " and color1 == old_color2:
|
||
aimg[k] += " "
|
||
old_char = gsval
|
||
elif gsval == " " and color1 == old_color1 and old_char == "█":
|
||
aimg[k] = aimg[k][:-1]
|
||
aimg[k] += "\x0301,{0} ".format(color1)
|
||
old_color1 = "01"
|
||
old_color2 = color1
|
||
old_char = gsval
|
||
elif gsval == " " and color1 == old_color1 and old_char == "^█":
|
||
aimg[k] = aimg[k][:-4]
|
||
aimg[k] += "\x0301,{0} ".format(color1)
|
||
old_color1 = "01"
|
||
old_color2 = color1
|
||
old_char = gsval
|
||
elif (
|
||
gsval == " "
|
||
and color1 == old_color1
|
||
and old_char == "^^▀"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] = aimg[k][:-7]
|
||
aimg[k] += "\x03{0},{1}▄ ".format(old_color2, color1)
|
||
old_color1 = old_color2
|
||
old_color2 = color1
|
||
old_char = gsval
|
||
elif (
|
||
gsval == " "
|
||
and color1 == old_color1
|
||
and old_char != "█"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] += "█"
|
||
old_char = "█"
|
||
elif gsval == " " and "tops" not in optlist:
|
||
aimg[k] += "\x03{0}█".format(color1)
|
||
old_color1 = color1
|
||
old_char = "^█"
|
||
elif (
|
||
gsval != " "
|
||
and color1 == old_color1
|
||
and old_char == "^█"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] = aimg[k][:-4]
|
||
aimg[k] += "\x03{0},{1} ▄".format(color2, color1)
|
||
old_color1 = color2
|
||
old_color2 = color1
|
||
old_char = "▄"
|
||
elif gsval != " " and color2 == old_color1 and old_char == "^█":
|
||
aimg[k] = aimg[k][:-4]
|
||
aimg[k] += "\x03{0},{1} ▀".format(color1, color2)
|
||
old_color1 = color1
|
||
old_color2 = color2
|
||
old_char = gsval
|
||
elif (
|
||
gsval != " "
|
||
and color1 == old_color2
|
||
and color2 == old_color1
|
||
and old_char == "^^▀"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] = aimg[k][:-7]
|
||
aimg[k] += "\x03{0},{1}▄▀".format(color1, color2)
|
||
old_color1 = color1
|
||
old_color2 = color2
|
||
old_char = gsval
|
||
elif (
|
||
gsval != " "
|
||
and color1 == old_color1
|
||
and color2 != old_color2
|
||
and old_char == "^^▀"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] = aimg[k][:-7]
|
||
aimg[k] += "\x03{0},{1}▄\x03{2}▄".format(
|
||
old_color2, color1, color2
|
||
)
|
||
old_color1 = color2
|
||
old_color2 = color1
|
||
old_char = "▄"
|
||
elif (
|
||
gsval != " "
|
||
and color1 == old_color1
|
||
and color2 != old_color2
|
||
and old_char == "^▀"
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] = aimg[k][:-4]
|
||
aimg[k] += "\x03{0},{1}▄\x03{2}▄".format(
|
||
old_color2, color1, color2
|
||
)
|
||
old_color1 = color2
|
||
old_color2 = color1
|
||
old_char = "▄"
|
||
elif (
|
||
gsval != " "
|
||
and color1 == old_color2
|
||
and color2 == old_color1
|
||
and "tops" not in optlist
|
||
):
|
||
aimg[k] += "▄"
|
||
old_char = "▄"
|
||
elif (
|
||
gsval != " " and color1 == old_color2 and "tops" not in optlist
|
||
):
|
||
aimg[k] += "\x03{0}▄".format(color2)
|
||
old_color1 = color2
|
||
old_char = "▄"
|
||
elif color1 != old_color1 and color2 == old_color2:
|
||
aimg[k] += "\x03{0}{1}".format(color1, gsval)
|
||
old_color1 = color1
|
||
if gsval == " ":
|
||
old_char = gsval
|
||
else:
|
||
old_char = "^▀"
|
||
else:
|
||
aimg[k] += "\x03{0},{1}{2}".format(color1, color2, gsval)
|
||
old_color1 = color1
|
||
old_color2 = color2
|
||
if gsval == " ":
|
||
old_char = gsval
|
||
else:
|
||
old_char = "^^▀"
|
||
if "tops" in optlist:
|
||
aimg[k] = re.sub("\x03\d\d,(\d\d\s+\x03)", "\x0301,\g<1>", aimg[k])
|
||
aimg[k] = re.sub("\x03\d\d,(\d\d\s+$)", "\x0301,\g<1>", aimg[k])
|
||
aimg[k] = re.sub("\x03\d\d,(\d\d\s\x03)", "\x0301,\g<1>", aimg[k])
|
||
aimg[k] = re.sub(
|
||
"\x0301,(\d\d)(\s+)\x03(\d\d)([^,])",
|
||
"\x03\g<3>,\g<1>\g<2>\g<4>",
|
||
aimg[k],
|
||
)
|
||
for i in range(0, 98):
|
||
i = "%02d" % i
|
||
aimg[k] = aimg[k].replace("{0}".format(i), "{0}".format(int(i)))
|
||
k += 1
|
||
else:
|
||
if "chars" not in optlist and gscale != "\xa0":
|
||
image = image.resize((cols, rows), resize)
|
||
image = image.convert("L")
|
||
lumamap = np.array(image)
|
||
# generate list of dimensions
|
||
char = 0
|
||
for j in range(rows):
|
||
# append an empty string
|
||
aimg.append("")
|
||
old_color = None
|
||
for i in range(cols):
|
||
if "chars" not in optlist and gscale != "\xa0":
|
||
# get average luminance
|
||
avg = int(np.average(lumamap[j][i]))
|
||
# look up ascii char
|
||
gsval = gscale[int((avg * (len(gscale) - 1)) / 255)]
|
||
elif "chars" in optlist and gscale != "\xa0":
|
||
if char < len(gscale):
|
||
gsval = gscale[char]
|
||
char += 1
|
||
else:
|
||
char = 0
|
||
gsval = gscale[char]
|
||
char += 1
|
||
else:
|
||
gsval = "\xa0"
|
||
# get color value
|
||
if type != "no-color" and gscale != "\xa0" and i == 0:
|
||
color = self.getColor(colormap[j][i].tolist(), speed)
|
||
old_color = color
|
||
if bg != 99:
|
||
color = "{0},{1}".format(color, "{:02d}".format(int(bg)))
|
||
if gsval != "\xa0":
|
||
aimg[j] += "\x03{0}{1}".format(color, gsval)
|
||
else:
|
||
aimg[j] += "\x030,{0} ".format(int(color))
|
||
elif type == "no-color" and i == 0:
|
||
if bg != 99 and fg != 99:
|
||
aimg[j] += "\x03{0},{1}{2}".format(
|
||
"{:02d}".format(int(fg)),
|
||
"{:02d}".format(int(bg)),
|
||
gsval,
|
||
)
|
||
elif fg != 99:
|
||
aimg[j] += "\x03{0}{1}".format(
|
||
"{:02d}".format(int(fg)), gsval
|
||
)
|
||
elif bg != 99:
|
||
aimg[j] += "\x03{0},{1}{2}".format(
|
||
"{:02d}".format(int(fg)),
|
||
"{:02d}".format(int(bg)),
|
||
gsval,
|
||
)
|
||
elif type != "no-color" and gsval != " ":
|
||
color = self.getColor(colormap[j][i].tolist(), speed)
|
||
if color != old_color:
|
||
old_color = color
|
||
# append ascii char to string
|
||
if gsval != "\xa0":
|
||
if gsval.isdigit():
|
||
color = "{:02d}".format(int(color))
|
||
aimg[j] += "\x03{0}{1}".format(color, gsval)
|
||
else:
|
||
aimg[j] += "\x03{0}{1}".format(int(color), gsval)
|
||
else:
|
||
aimg[j] += "\x030,{0} ".format(int(color))
|
||
else:
|
||
aimg[j] += "{0}".format(gsval)
|
||
else:
|
||
aimg[j] += "{0}".format(gsval)
|
||
output = aimg
|
||
self.stopped[channel] = False
|
||
end_time = time.time()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("showStats", msg.args[0]):
|
||
longest = len(max(output, key=len).encode("utf-8"))
|
||
render_time = "{0:.2f}".format(end_time - start_time)
|
||
irc.reply(
|
||
"[Source Colors: {0}, Render Time: {1} seconds, Longest Line: {2}"
|
||
" bytes]".format(self.source_colors, render_time, longest),
|
||
prefixNick=False,
|
||
)
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(self.doPaste(url, paste), private=False, notice=False, to=channel)
|
||
|
||
img = wrap(
|
||
img,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"w": "int",
|
||
"invert": "",
|
||
"fast": "",
|
||
"slow": "",
|
||
"16": "",
|
||
"99": "",
|
||
"83": "",
|
||
"delay": "float",
|
||
"resize": "int",
|
||
"quantize": "",
|
||
"no-quantize": "",
|
||
"chars": "text",
|
||
"bg": "int",
|
||
"fg": "int",
|
||
"ramp": "text",
|
||
"no-color": "",
|
||
"block": "",
|
||
"ascii": "",
|
||
"1/2": "",
|
||
"s": "float",
|
||
"tops": "",
|
||
}
|
||
),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def scroll(self, irc, msg, args, channel, optlist, url):
|
||
"""[<channel>] [--delay] <url>
|
||
Play IRC art files from web links.
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
self.stopped[channel] = False
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if url.startswith("https://paste.ee/p/"):
|
||
url = url.replace("https://paste.ee/p/", "https://paste.ee/r/")
|
||
elif url.startswith("https://pastebin.com/") and "/raw/" not in url:
|
||
url = url.replace("https://pastebin.com/", "https://pastebin.com/raw/")
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
try:
|
||
r = requests.get(url, headers=header, stream=True, timeout=10)
|
||
r.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for scroll: {0}".format(e))
|
||
return
|
||
if "text/plain" in r.headers["content-type"]:
|
||
try:
|
||
file = r.content.decode().replace("\r\n", "\n")
|
||
except:
|
||
file = r.text.replace("\r\n", "\n")
|
||
else:
|
||
irc.reply("Invalid file type.", private=False, notice=False)
|
||
return
|
||
output = file.splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
|
||
scroll = wrap(scroll, [optional("channel"), getopts({"delay": "float"}), "text"])
|
||
|
||
def a2m(self, irc, msg, args, channel, optlist, url):
|
||
"""[<channel>] [--delay] [--l] [--r] [--n] [--p] [--t] [--w] <url>
|
||
Convert ANSI files to IRC formatted text. https://github.com/tat3r/a2m
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
opts = ""
|
||
if "l" in optlist:
|
||
l = optlist.get("l")
|
||
opts += "-l {0} ".format(l)
|
||
if "r" in optlist:
|
||
r = optlist.get("r")
|
||
opts += "-r {0} ".format(r)
|
||
if "n" in optlist:
|
||
n = optlist.get("n")
|
||
opts += "-n {0}".format(n)
|
||
if "p" in optlist:
|
||
opts += "-p "
|
||
if "t" in optlist:
|
||
t = optlist.get("t")
|
||
opts += "-t {0} ".format(t)
|
||
if "w" in optlist:
|
||
w = optlist.get("w")
|
||
opts += "-w {0} ".format(w)
|
||
else:
|
||
opts += "-w 80 "
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
try:
|
||
r = requests.get(url, stream=True, headers=header, timeout=10)
|
||
r.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for a2m: {0}".format(e))
|
||
return
|
||
try:
|
||
if (
|
||
"text/plain" in r.headers["content-type"]
|
||
or "application/octet-stream" in r.headers["content-type"]
|
||
and int(r.headers["content-length"]) < 1000000
|
||
):
|
||
path = os.path.dirname(os.path.abspath(__file__))
|
||
filepath = "{0}/tmp".format(path)
|
||
filename = "{0}/{1}".format(filepath, url.split("/")[-1])
|
||
open(filename, "wb").write(r.content.replace(b";5;", b";"))
|
||
try:
|
||
output = pexpect.run(
|
||
"a2m {0} {1}".format(opts.strip(), str(filename))
|
||
)
|
||
try:
|
||
os.remove(filename)
|
||
except:
|
||
pass
|
||
except:
|
||
irc.reply(
|
||
"Error. Have you installed A2M? https://github.com/tat3r/a2m",
|
||
private=False,
|
||
notice=False,
|
||
)
|
||
return
|
||
else:
|
||
irc.reply("Invalid file type.")
|
||
return
|
||
except:
|
||
irc.reply("Invalid file type.")
|
||
return
|
||
self.stopped[channel] = False
|
||
output = re.sub("(\x03(\d+).*)\x03,", "\g<1>\x03\g<2>,", output.decode())
|
||
output = output.splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(self.doPaste(url, paste), private=False, notice=False, to=channel)
|
||
|
||
a2m = wrap(
|
||
a2m,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"l": "int",
|
||
"r": "int",
|
||
"t": "int",
|
||
"w": "int",
|
||
"p": "",
|
||
"delay": "float",
|
||
}
|
||
),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def p2u(self, irc, msg, args, channel, optlist, url):
|
||
"""[<channel>] [--b] [--f] [--p] [--s] [--t] [--w] [--delay] <url>
|
||
Picture to Unicode. https://git.trollforge.org/p2u/about/
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
opts = ""
|
||
if "b" in optlist:
|
||
b = optlist.get("b")
|
||
opts += "-b {0} ".format(b)
|
||
if "f" in optlist:
|
||
f = optlist.get("f")
|
||
opts += "-f {0} ".format(f)
|
||
else:
|
||
opts += "-f m "
|
||
if "p" in optlist:
|
||
p = optlist.get("p")
|
||
opts += "-p {0} ".format(p)
|
||
else:
|
||
opts += "-p x "
|
||
if "s" in optlist:
|
||
s = optlist.get("s")
|
||
opts += "-s {0} ".format(s)
|
||
if "t" in optlist:
|
||
t = optlist.get("t")
|
||
opts += "-t {0} ".format(t)
|
||
if "w" in optlist:
|
||
w = optlist.get("w")
|
||
opts += "-w {0} ".format(w)
|
||
else:
|
||
w = self.registryValue("blockWidth", msg.args[0])
|
||
opts += "-w {0} ".format(w)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
path = os.path.dirname(os.path.abspath(__file__))
|
||
filepath = "{0}/tmp".format(path)
|
||
filename = "{0}/{1}".format(filepath, url.split("/")[-1])
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
image_formats = ("image/png", "image/jpeg", "image/jpg", "image/gif")
|
||
try:
|
||
r = requests.get(url, stream=True, headers=header, timeout=10)
|
||
r.raise_for_status()
|
||
except (
|
||
requests.exceptions.RequestException,
|
||
requests.exceptions.HTTPError,
|
||
) as e:
|
||
log.debug("TextArt: error retrieving data for p2u: {0}".format(e))
|
||
return
|
||
if r.headers["content-type"] in image_formats and r.status_code == 200:
|
||
with open("{0}".format(filename), "wb") as f:
|
||
f.write(r.content)
|
||
try:
|
||
output = pexpect.run(
|
||
"p2u -f m {0} {1}".format(opts.strip(), str(filename))
|
||
)
|
||
try:
|
||
os.remove(filename)
|
||
except:
|
||
pass
|
||
except:
|
||
irc.reply(
|
||
"Error. Have you installed p2u? https://git.trollforge.org/p2u",
|
||
private=False,
|
||
notice=False,
|
||
)
|
||
return
|
||
else:
|
||
irc.reply("Invalid file type.", private=False, notice=False)
|
||
return
|
||
self.stopped[channel] = False
|
||
output = output.decode().splitlines()
|
||
output = [re.sub("^\x03 ", " ", line) for line in output]
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(self.doPaste(url, paste), private=False, notice=False, to=channel)
|
||
else:
|
||
irc.reply(
|
||
"Unexpected file type or link format", private=False, notice=False
|
||
)
|
||
|
||
p2u = wrap(
|
||
p2u,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"b": "int",
|
||
"f": "text",
|
||
"p": "text",
|
||
"s": "int",
|
||
"t": "int",
|
||
"w": "int",
|
||
"delay": "float",
|
||
}
|
||
),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def tdf(self, irc, msg, args, channel, optlist, text):
|
||
"""[<channel>] [--f] [--j] [--w] [--e] [--r] [--i][--delay] <text>
|
||
Text to TheDraw ANSI Fonts. http://www.roysac.com/thedrawfonts-tdf.html
|
||
--f [font] Specify font file used.
|
||
--j l|r|c Justify left, right, or center. Default is left.
|
||
--w n Set screen width. Default is 80.
|
||
--c a|m Color format ANSI or mirc. Default is ANSI.
|
||
--i Print font details.
|
||
--r Use random font.
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
if len(text) > self.registryValue("maxLength", msg.channel):
|
||
return
|
||
if len(text.split(" ")) > self.registryValue("maxWords", msg.channel):
|
||
return
|
||
optlist = dict(optlist)
|
||
opts = ""
|
||
if "f" in optlist:
|
||
f = optlist.get("f")
|
||
opts += "-f {0} ".format(f.lower())
|
||
else:
|
||
opts += "-r "
|
||
if "j" in optlist:
|
||
j = optlist.get("j")
|
||
opts += "-j {0} ".format(j)
|
||
if "w" in optlist:
|
||
w = optlist.get("w")
|
||
opts += "-w {0} ".format(w)
|
||
else:
|
||
opts += "-w 80 "
|
||
if "e" in optlist:
|
||
e = optlist.get("e")
|
||
opts += "-e {0} ".format(e)
|
||
if "r" in optlist:
|
||
opts += "-r "
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if "i" in optlist:
|
||
opts += "-i "
|
||
try:
|
||
output = pexpect.run(
|
||
"tdfiglet -c m {0} {1}".format(opts.strip(), r"{}".format(text))
|
||
)
|
||
except:
|
||
irc.reply(
|
||
"Error. Have you installed tdfiglet? https://github.com/tat3r/tdfiglet",
|
||
private=False,
|
||
notice=False,
|
||
)
|
||
return
|
||
self.stopped[channel] = False
|
||
output = output.decode().replace("\r\r\n", "\r\n")
|
||
output = re.sub("\x03\x03\s*", "\x0F ", output)
|
||
output = re.sub("\x0F\s*\x03$", "", output)
|
||
output = output.splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(
|
||
self.doPaste(text, paste), private=False, notice=False, to=channel
|
||
)
|
||
|
||
tdf = wrap(
|
||
tdf,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"f": "text",
|
||
"j": "text",
|
||
"w": "int",
|
||
"e": "text",
|
||
"r": "",
|
||
"i": "",
|
||
"delay": "float",
|
||
}
|
||
),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def toilet(self, irc, msg, args, channel, optlist, text):
|
||
"""[<channel>] [--f fontname] [--F filter1,filter2,etc.] [--w] [--delay] <text>
|
||
Text to toilet figlets. -f to select font. -F to select filters. Separate multiple filters with a comma.
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
if len(text) > self.registryValue("maxLength", msg.channel):
|
||
return
|
||
if len(text.split(" ")) > self.registryValue("maxWords", msg.channel):
|
||
return
|
||
optlist = dict(optlist)
|
||
opts = ""
|
||
if "f" in optlist:
|
||
f = optlist.get("f")
|
||
opts += "-f {0} ".format(f)
|
||
if "F" in optlist:
|
||
filter = optlist.get("F")
|
||
if "," in filter:
|
||
filter = filter.split(",")
|
||
for i in range(len(filter)):
|
||
opts += "-F {0} ".format(filter[i])
|
||
else:
|
||
opts += "-F {0} ".format(filter)
|
||
if "w" in optlist:
|
||
w = optlist.get("w")
|
||
opts += "-w {0} ".format(w)
|
||
elif "W" in optlist:
|
||
opts += "-W "
|
||
else:
|
||
opts += "-w 100 "
|
||
if "s" in optlist:
|
||
opts += "-s "
|
||
elif "k" in optlist:
|
||
opts += "-k "
|
||
elif "o" in optlist:
|
||
opts += "-o "
|
||
elif "S" in optlist:
|
||
opts += "-S "
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
try:
|
||
output = pexpect.run("toilet --irc {0} {1}".format(opts.strip(), text))
|
||
except:
|
||
irc.reply("Error. Have you installed toilet?", private=False, notice=False)
|
||
return
|
||
self.stopped[channel] = False
|
||
output = output.decode().replace("\r\r\n", "\r\n")
|
||
output = output.splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(
|
||
self.doPaste(text, paste), private=False, notice=False, to=channel
|
||
)
|
||
|
||
toilet = wrap(
|
||
toilet,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"f": "text",
|
||
"F": "text",
|
||
"s": "",
|
||
"S": "",
|
||
"k": "",
|
||
"w": "int",
|
||
"W": "",
|
||
"o": "",
|
||
"delay": "float",
|
||
}
|
||
),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def fonts(self, irc, msg, args, optlist):
|
||
"""[--toilet]
|
||
List figlets. Default list are tdf fonts. --toilet for toilet fonts
|
||
"""
|
||
optlist = dict(optlist)
|
||
if "toilet" in optlist:
|
||
try:
|
||
reply = ", ".join(sorted(os.listdir("/usr/share/figlet")))
|
||
irc.reply(reply, prefixNick=False)
|
||
except:
|
||
irc.reply("Sorry, unable to access font directory /usr/share/figlet")
|
||
else:
|
||
try:
|
||
reply = ", ".join(
|
||
sorted(os.listdir("/usr/local/share/tdfiglet/fonts/"))
|
||
)
|
||
irc.reply(
|
||
"http://www.roysac.com/thedrawfonts-tdf.html", prefixNick=False
|
||
)
|
||
irc.reply(reply, prefixNick=False)
|
||
except FileNotFoundError:
|
||
reply = ", ".join(sorted(os.listdir("/usr/share/figlet")))
|
||
irc.reply(reply, prefixNick=False)
|
||
except:
|
||
irc.reply(
|
||
"Sorry, unable to access font directories"
|
||
" /usr/local/share/tdfiglet/fonts/ or /usr/share/figlet"
|
||
)
|
||
|
||
fonts = wrap(fonts, [getopts({"toilet": ""})])
|
||
|
||
def wttr(self, irc, msg, args, channel, optlist, location):
|
||
"""[<channel>] [--16] [--99] <location/moon>
|
||
IRC art weather report from wttr.in for <location>.
|
||
--16 for 16 colors (default).
|
||
--99 for 99 colors.
|
||
Get moon phase with 'wttr moon'.
|
||
<location>?u (use imperial units).
|
||
<location>?m (metric).
|
||
<location>?<1-3> (number of days)
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
if "format=j" in location.lower():
|
||
return
|
||
optlist = dict(optlist)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if "16" in optlist:
|
||
self.colors = 16
|
||
elif "99" in optlist:
|
||
self.colors = 99
|
||
else:
|
||
self.colors = self.registryValue("colors", msg.args[0])
|
||
file = requests.get("http://wttr.in/{0}".format(location), timeout=10)
|
||
output = file.content.decode()
|
||
output = self.ansi2irc(output)
|
||
output = re.sub("⚡", "☇ ", output)
|
||
output = re.sub("‘‘", "‘ ", output)
|
||
output = re.sub("\n\nFollow.*$", "", output)
|
||
self.stopped[channel] = False
|
||
output = output.splitlines()
|
||
output = [line.strip("\x0F") for line in output]
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(
|
||
self.doPaste(location, paste), private=False, notice=False, to=channel
|
||
)
|
||
|
||
wttr = wrap(
|
||
wttr,
|
||
[
|
||
optional("channel"),
|
||
getopts({"delay": "float", "16": "", "99": "", "fast": "", "slow": ""}),
|
||
"text",
|
||
],
|
||
)
|
||
|
||
def rate(self, irc, msg, args, channel, optlist, coin):
|
||
"""[<channel>] [--16] [--99] [--sub <text>] [coin]
|
||
Crypto exchange rate info from rate.sx. http://rate.sx/:help. Use --sub to
|
||
set subdomain e.g. eur, btc, etc. Get a graph with [coin] e.g. 'rate btc'.
|
||
--16 for 16 colors (default).
|
||
--99 for 99 colors.
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
if "16" in optlist:
|
||
self.colors = 16
|
||
elif "99" in optlist:
|
||
self.colors = 99
|
||
else:
|
||
self.colors = self.registryValue("colors", msg.args[0])
|
||
if "sub" in optlist:
|
||
sub = optlist.get("sub")
|
||
else:
|
||
sub = "usd"
|
||
if not coin:
|
||
coin = ""
|
||
file = requests.get("http://{0}.rate.sx/{1}".format(sub, coin), timeout=10)
|
||
output = file.content.decode()
|
||
output = self.ansi2irc(output)
|
||
output = output.replace("\x1b(B", "")
|
||
output = re.sub(r"\n\x0307NEW FEATURE:.*\n.*", "", output).strip()
|
||
output = output.splitlines()
|
||
output = [line.strip("\x0F") for line in output]
|
||
self.stopped[channel] = False
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
paste = ""
|
||
for line in output:
|
||
if not line.strip():
|
||
line = "\xa0"
|
||
paste += line + "\n"
|
||
if self.registryValue("pasteEnable", msg.args[0]):
|
||
irc.reply(
|
||
self.doPaste(coin, paste), private=False, notice=False, to=channel
|
||
)
|
||
|
||
rate = wrap(
|
||
rate,
|
||
[
|
||
optional("channel"),
|
||
getopts(
|
||
{
|
||
"delay": "float",
|
||
"16": "",
|
||
"99": "",
|
||
"sub": "text",
|
||
"fast": "",
|
||
"slow": "",
|
||
}
|
||
),
|
||
optional("text"),
|
||
],
|
||
)
|
||
|
||
def fortune(self, irc, msg, args, channel, optlist):
|
||
"""[<channel>]
|
||
Returns random art fortune from http://www.asciiartfarts.com/fortune.txt
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
self.stopped[channel] = False
|
||
data = requests.get("http://www.asciiartfarts.com/fortune.txt", timeout=10)
|
||
fortunes = data.content.decode().split("%\n")
|
||
fortune = random.randrange(0, len(fortunes))
|
||
output = fortunes[fortune].splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
|
||
fortune = wrap(fortune, [optional("channel"), getopts({"delay": "float"})])
|
||
|
||
def mircart(self, irc, msg, args, channel, optlist, search):
|
||
"""[<channel>] (search text)
|
||
Search https://mircart.org/ and scroll first result
|
||
"""
|
||
if not channel:
|
||
channel = msg.args[0]
|
||
if channel != msg.args[0] and not ircdb.checkCapability(msg.prefix, "admin"):
|
||
irc.errorNoCapability("admin")
|
||
return
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
self.stopped[channel] = False
|
||
ua = random.choice(self.agents)
|
||
header = {"User-Agent": ua}
|
||
data = requests.get(
|
||
"https://mircart.org/?s={0}".format(search), headers=header, timeout=10
|
||
)
|
||
if not data:
|
||
irc.reply("Error: No results found for {0}".format(search))
|
||
return
|
||
soup = BeautifulSoup(data.content)
|
||
url = soup.find(href=re.compile(".txt"))
|
||
if not url:
|
||
irc.reply("Error: No results found for {0}".format(search))
|
||
return
|
||
data = requests.get(url.get("href"), headers=header, timeout=10)
|
||
try:
|
||
output = data.content.decode()
|
||
except:
|
||
output = data.text
|
||
output = output.splitlines()
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
irc.reply(url.get("href"))
|
||
|
||
mircart = wrap(mircart, [optional("channel"), getopts({"delay": "float"}), "text"])
|
||
|
||
def cq(self, irc, msg, args):
|
||
"""
|
||
Stop the scroll.
|
||
"""
|
||
channel = msg.args[0]
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
self.stopped.setdefault(channel, None)
|
||
if not self.stopped[channel]:
|
||
irc.reply("Stopping.")
|
||
self.stopped[channel] = True
|
||
|
||
cq = wrap(cq)
|
||
|
||
def codes(self, irc, msg, args, optlist):
|
||
"""
|
||
Show a grid of IRC color codes.
|
||
"""
|
||
channel = msg.args[0]
|
||
if not irc.isChannel(channel):
|
||
channel = msg.nick
|
||
optlist = dict(optlist)
|
||
if "delay" in optlist and ircdb.checkCapability(msg.prefix, "admin"):
|
||
delay = optlist.get("delay")
|
||
else:
|
||
delay = self.registryValue("delay", msg.args[0])
|
||
output = []
|
||
output.append(
|
||
"\x031,0000\x031,0101\x031,0202\x031,0303\x031,0404\x031,0505\x031,0606\x031,0707\x031,0808\x031,0909\x031,1010\x031,1111\x031,1212\x031,1313\x031,1414\x031,1515",
|
||
)
|
||
output.append(
|
||
"\x031,1616\x031,1717\x031,1818\x031,1919\x031,2020\x031,2121\x031,2222\x031,2323\x031,2424\x031,2525\x031,2626\x031,2727",
|
||
)
|
||
output.append(
|
||
"\x031,2828\x031,2929\x031,3030\x031,3131\x031,3232\x031,3333\x031,3434\x031,3535\x031,3636\x031,3737\x031,3838\x031,3939",
|
||
)
|
||
output.append(
|
||
"\x031,4040\x031,4141\x031,4242\x031,4343\x031,4444\x031,4545\x031,4646\x031,4747\x031,4848\x031,4949\x031,5050\x031,5151",
|
||
)
|
||
output.append(
|
||
"\x031,5252\x031,5353\x031,5454\x031,5555\x031,5656\x031,5757\x031,5858\x031,5959\x031,6060\x031,6161\x031,6262\x031,6363",
|
||
)
|
||
output.append(
|
||
"\x031,6464\x031,6565\x031,6666\x031,6767\x031,6868\x031,6969\x031,7070\x031,7171\x031,7272\x031,7373\x031,7474\x031,7575",
|
||
)
|
||
output.append(
|
||
"\x031,7676\x031,7777\x031,7878\x031,7979\x031,8080\x031,8181\x031,8282\x031,8383\x031,8484\x031,8585\x031,8686\x031,8787",
|
||
)
|
||
output.append(
|
||
"\x031,8888\x031,8989\x031,9090\x031,9191\x031,9292\x031,9393\x031,9494\x031,9595\x031,9696\x031,9797\x031,9898\x031,9999",
|
||
)
|
||
asyncio.run(self.reply(irc, output, channel, delay))
|
||
|
||
codes = wrap(codes, [getopts({"delay": "float"})])
|
||
|
||
|
||
Class = TextArt
|