import logging
import ostools
PchumLog = logging.getLogger('pchumLogger')
try:
from PyQt6 import QtGui
except ImportError:
print("PyQt5 fallback (dataobjs.py)")
from PyQt5 import QtGui
from datetime import datetime
import re
import random
from mood import Mood
from parsetools import (timeDifference,
convertTags,
lexMessage,
parseRegexpFunctions,
smiledict)
from mispeller import mispeller
_urlre = re.compile(r"(?i)(?:^|(?<=\s))(?:(?:https?|ftp)://|magnet:)[^\s]+")
#_url2re = re.compile(r"(?i)(?\\]+)\)")
_lowerre = re.compile(r"lower\(([\w<>\\]+)\)")
_scramblere = re.compile(r"scramble\(([\w<>\\]+)\)")
_reversere = re.compile(r"reverse\(([\w<>\\]+)\)")
_ctagre = re.compile("(?c=?.*?>)", re.I)
_smilere = re.compile("|".join(list(smiledict.keys())))
_memore = re.compile(r"(\s|^)(#[A-Za-z0-9_]+)")
_handlere = re.compile(r"(\s|^)(@[A-Za-z0-9_]+)")
class pesterQuirk(object):
def __init__(self, quirk):
if type(quirk) != dict:
raise ValueError("Quirks must be given a dictionary")
self.quirk = quirk
self.type = self.quirk["type"]
if "on" not in self.quirk:
self.quirk["on"] = True
self.on = self.quirk["on"]
if "group" not in self.quirk:
self.quirk["group"] = "Miscellaneous"
self.group = self.quirk["group"]
try:
self.checkstate = self.quirk["checkstate"]
except KeyError:
pass
def apply(self, string, first=False, last=False):
if not self.on:
return string
elif self.type == "prefix":
return self.quirk["value"] + string
elif self.type == "suffix":
return string + self.quirk["value"]
elif self.type == "replace":
return string.replace(self.quirk["from"], self.quirk["to"])
elif self.type == "regexp":
fr = self.quirk["from"]
if not first and len(fr) > 0 and fr[0] == "^":
return string
if not last and len(fr) > 0 and fr[len(fr)-1] == "$":
return string
to = self.quirk["to"]
pt = parseRegexpFunctions(to)
return re.sub(fr, pt.expand, string)
elif self.type == "random":
if len(self.quirk["randomlist"]) == 0:
return string
fr = self.quirk["from"]
if not first and len(fr) > 0 and fr[0] == "^":
return string
if not last and len(fr) > 0 and fr[len(fr)-1] == "$":
return string
def randomrep(mo):
choice = random.choice(self.quirk["randomlist"])
pt = parseRegexpFunctions(choice)
return pt.expand(mo)
return re.sub(self.quirk["from"], randomrep, string)
elif self.type == "spelling":
percentage = self.quirk["percentage"]/100.0
words = string.split(" ")
newl = []
ctag = re.compile("(?c=?.*?>)", re.I)
for w in words:
p = random.random()
if not ctag.search(w) and p < percentage:
newl.append(mispeller(w))
elif p < percentage:
split = ctag.split(w)
tmp = []
for s in split:
if s and not ctag.search(s):
tmp.append(mispeller(s))
else:
tmp.append(s)
newl.append("".join(tmp))
else:
newl.append(w)
return " ".join(newl)
def __str__(self):
if self.type == "prefix":
return "BEGIN WITH: %s" % (self.quirk["value"])
elif self.type == "suffix":
return "END WITH: %s" % (self.quirk["value"])
elif self.type == "replace":
return "REPLACE %s WITH %s" % (self.quirk["from"], self.quirk["to"])
elif self.type == "regexp":
return "REGEXP: %s REPLACED WITH %s" % (self.quirk["from"], self.quirk["to"])
elif self.type == "random":
return "REGEXP: %s RANDOMLY REPLACED WITH %s" % (self.quirk["from"], [r for r in self.quirk["randomlist"]])
elif self.type == "spelling":
return "MISPELLER: %d%%" % (self.quirk["percentage"])
class pesterQuirks(object):
def __init__(self, quirklist):
self.quirklist = []
for q in quirklist:
self.addQuirk(q)
def plainList(self):
return [q.quirk for q in self.quirklist]
def addQuirk(self, q):
if type(q) == dict:
self.quirklist.append(pesterQuirk(q))
elif type(q) == pesterQuirk:
self.quirklist.append(q)
def apply(self, lexed, first=False, last=False):
prefix = [q for q in self.quirklist if q.type=='prefix']
suffix = [q for q in self.quirklist if q.type=='suffix']
newlist = []
for (i, o) in enumerate(lexed):
if type(o) not in [str, str]:
if i == 0:
string = " "
for p in prefix:
string += p.apply(string)
newlist.append(string)
newlist.append(o)
continue
lastStr = (i == len(lexed)-1)
string = o
for q in self.quirklist:
try:
checkstate = int(q.checkstate)
except Exception:
checkstate = 0
# Exclude option is checked
if checkstate == 2:
# Check for substring that should be excluded.
excludes = list()
# Check for links, store in list.
for match in re.finditer(_urlre, string):
excludes.append(match)
# Check for smilies, store in list.
for match in re.finditer(_smilere, string):
excludes.append(match)
# Check for @handles, store in list.
for match in re.finditer(_handlere, string):
excludes.append(match)
# Check for #memos, store in list.
for match in re.finditer(_memore, string):
excludes.append(match)
if len(excludes) >= 1:
# SORT !!!
excludes.sort(key=lambda exclude: exclude.start())
# Recursion check.
# Strings like http://:3: require this.
for n in range(0, len(excludes)-1):
if excludes[n].end() > excludes[n+1].start():
excludes.pop(n)
# Seperate parts to be quirked.
sendparts = list()
# Add string until start of exclude at index 0.
until = excludes[0].start()
sendparts.append(string[:until])
# Add strings between excludes.
for part in range(1, len(excludes)):
after = excludes[part-1].end()
until = excludes[part].start()
sendparts.append(string[after:until])
# Add string after exclude at last index.
after = excludes[-1].end()
sendparts.append(string[after:])
# Quirk to-be-quirked parts.
recvparts = list()
for part in sendparts:
# No split, apply like normal.
if q.type == 'regexp' or q.type == 'random':
recvparts.append(q.apply(part,
first=(i==0),
last=lastStr))
elif q.type == 'prefix' and i == 0:
recvparts.append(q.apply(part))
elif q.type == 'suffix' and lastStr:
recvparts.append(q.apply(part))
else:
recvparts.append(q.apply(part))
# Reconstruct and update string.
string = ''
#print("excludes: " + str(excludes))
#print("sendparts: " + str(sendparts))
#print("recvparts: " + str(recvparts))
for part in range(0, len(excludes)):
string += recvparts[part]
string += excludes[part].group()
string += recvparts[-1]
else:
# No split, apply like normal.
if q.type != 'prefix' and q.type != 'suffix':
if q.type == 'regexp' or q.type == 'random':
string = q.apply(string,
first=(i==0),
last=lastStr)
else:
string = q.apply(string)
elif q.type == 'prefix' and i == 0:
string = q.apply(string)
elif q.type == 'suffix' and lastStr:
string = q.apply(string)
else:
# No split, apply like normal.
if q.type != 'prefix' and q.type != 'suffix':
if q.type == 'regexp' or q.type == 'random':
string = q.apply(string,
first=(i==0),
last=lastStr)
else:
string = q.apply(string)
elif q.type == 'prefix' and i == 0:
string = q.apply(string)
elif q.type == 'suffix' and lastStr:
string = q.apply(string)
newlist.append(string)
final = []
for n in newlist:
if type(n) in [str, str]:
final.extend(lexMessage(n))
else:
final.append(n)
return final
def __iter__(self):
for q in self.quirklist:
yield q
class PesterProfile(object):
def __init__(self, handle, color=None, mood=Mood("offline"), group=None, notes="", chumdb=None):
self.handle = handle
if color is None:
if chumdb:
color = chumdb.getColor(handle, QtGui.QColor("black"))
else:
color = QtGui.QColor("black")
self.color = color
self.mood = mood
if group is None:
if chumdb:
group = chumdb.getGroup(handle, "Chums")
else:
group = "Chums"
self.group = group
self.notes = notes
def initials(self, time=None):
handle = self.handle
caps = [l for l in handle if l.isupper()]
if not caps:
caps = [""]
PchumLog.debug("handle = " + str(handle))
PchumLog.debug("caps = " + str(caps))
# Fallback for invalid string
try:
initials = (handle[0]+caps[0]).upper()
except:
PchumLog.exception('')
initials = "XX"
PchumLog.debug("initials = " + str(initials))
if hasattr(self, 'time') and time:
if self.time > time:
return "F"+initials
elif self.time < time:
return "P"+initials
else:
return "C"+initials
else:
return initials
def colorhtml(self):
if self.color:
return self.color.name()
else:
return "#000000"
def colorcmd(self):
if self.color:
(r, g, b, a) = self.color.getRgb()
return "%d,%d,%d" % (r,g,b)
else:
return "0,0,0"
def plaindict(self):
return (self.handle, {"handle": self.handle,
"mood": self.mood.name(),
"color": str(self.color.name()),
"group": str(self.group),
"notes": str(self.notes)})
def blocked(self, config):
return self.handle in config.getBlocklist()
def memsg(self, syscolor, lexmsg, time=None):
suffix = lexmsg[0].suffix
msg = convertTags(lexmsg[1:], "text")
uppersuffix = suffix.upper()
if time is not None:
handle = "%s %s" % (time.temporal, self.handle)
initials = time.pcf+self.initials()+time.number+uppersuffix
else:
handle = self.handle
initials = self.initials()+uppersuffix
return "-- %s%s [%s] %s --" % (syscolor.name(), handle, suffix, self.colorhtml(), initials, msg)
def pestermsg(self, otherchum, syscolor, verb):
return "-- %s [%s] %s %s [%s] at %s --" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb, otherchum.handle, otherchum.colorhtml(), otherchum.initials(), datetime.now().strftime("%H:%M"))
def moodmsg(self, mood, syscolor, theme):
return "-- %s [%s] changed their mood to %s --" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), mood.name().upper(), theme["main/chums/moods"][mood.name()]["icon"])
def idlemsg(self, syscolor, verb):
return "-- %s [%s] %s --" % (syscolor.name(), self.handle, self.colorhtml(), self.initials(), verb)
def memoclosemsg(self, syscolor, initials, verb):
if type(initials) == type(list()):
return "%s %s." % (syscolor.name(), self.colorhtml(), ", ".join(initials), verb)
else:
return "%s%s%s %s." % (syscolor.name(), self.colorhtml(), initials.pcf, self.initials(), initials.number, verb)
def memonetsplitmsg(self, syscolor, initials):
if len(initials) <= 0:
return "Netsplit quits: None" % (syscolor.name())
else:
return "Netsplit quits: %s" % (syscolor.name(), ", ".join(initials))
def memoopenmsg(self, syscolor, td, timeGrammar, verb, channel):
(temporal, pcf, when) = (timeGrammar.temporal, timeGrammar.pcf, timeGrammar.when)
timetext = timeDifference(td)
PchumLog.debug("pre pcf+self.initials()")
initials = pcf+self.initials()
PchumLog.debug("post pcf+self.initials()")
return "%s %s %s %s." % \
(syscolor.name(), self.colorhtml(), initials, timetext, verb, channel[1:].upper().replace("_", " "))
def memobanmsg(self, opchum, opgrammar, syscolor, initials, reason):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
if type(initials) == type(list()):
if opchum.handle == reason:
return "%s banned %s from responding to memo." % \
(opchum.colorhtml(), opinit, self.colorhtml(), ", ".join(initials))
else:
return "%s banned %s from responding to memo: [%s]." % \
(opchum.colorhtml(), opinit, self.colorhtml(), ", ".join(initials), str(reason))
else:
# Is timeGrammar defined? Not sure if this works as intented, added try except block to be safe.
try:
initials = timeGrammar.pcf+self.initials()+timeGrammar.number
if opchum.handle == reason:
return "%s banned %s from responding to memo." % \
(opchum.colorhtml(), opinit, self.colorhtml(), initials)
else:
return "%s banned %s from responding to memo: [%s]." % \
(opchum.colorhtml(), opinit, self.colorhtml(), initials, str(reason))
except:
PchumLog.exception('')
initials = self.initials()
if opchum.handle == reason:
return "%s banned %s from responding to memo." % \
(opchum.colorhtml(), opinit, self.colorhtml(), initials)
else:
return "%s banned %s from responding to memo: [%s]." % \
(opchum.colorhtml(), opinit, self.colorhtml(), initials, str(reason))
# As far as I'm aware, there's no IRC reply for this, this seems impossible to check for in practice.
def memopermabanmsg(self, opchum, opgrammar, syscolor, timeGrammar):
initials = (timeGrammar.pcf
+ self.initials()
+ timeGrammar.number)
opinit = (opgrammar.pcf
+ opchum.initials()
+ opgrammar.number)
return "%s permabanned %s from the memo." % \
(opchum.colorhtml(), opinit, self.colorhtml(), initials)
def memojoinmsg(self, syscolor, td, timeGrammar, verb):
#(temporal, pcf, when) = (timeGrammar.temporal, timeGrammar.pcf, timeGrammar.when)
timetext = timeDifference(td)
initials = timeGrammar.pcf+self.initials()+timeGrammar.number
return "%s %s [%s] %s %s." % \
(syscolor.name(), self.colorhtml(), timeGrammar.temporal, self.handle,
initials, timetext, verb)
def memoopmsg(self, opchum, opgrammar, syscolor):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
return "%s made %s an OP." % \
(opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
def memodeopmsg(self, opchum, opgrammar, syscolor):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
return "%s took away %s's OP powers." % \
(opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
def memovoicemsg(self, opchum, opgrammar, syscolor):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
return "%s gave %s voice." % \
(opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
def memodevoicemsg(self, opchum, opgrammar, syscolor):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
return "%s took away %s's voice." % \
(opchum.colorhtml(), opinit, self.colorhtml(), self.initials())
def memomodemsg(self, opchum, opgrammar, syscolor, modeverb, modeon):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
if modeon: modeon = "now"
else: modeon = "no longer"
return "Memo is %s %s by %s" % \
(syscolor.name(), modeon, modeverb, opchum.colorhtml(), opinit)
def memoquirkkillmsg(self, opchum, opgrammar, syscolor):
opinit = opgrammar.pcf+opchum.initials()+opgrammar.number
return "%s turned off your quirk." % \
(syscolor.name(), opchum.colorhtml(), opinit)
@staticmethod
def checkLength(handle):
return len(handle) <= 256
@staticmethod
def checkValid(handle):
caps = [l for l in handle if l.isupper()]
if len(caps) != 1:
return (False, "Must have exactly 1 uppercase letter")
if handle[0].isupper():
return (False, "Cannot start with uppercase letter")
if re.search("[^A-Za-z0-9]", handle) is not None:
return (False, "Only alphanumeric characters allowed")
return (True,)
class PesterHistory(object):
def __init__(self):
self.history = []
self.current = 0
self.saved = None
def next(self, text):
if self.current == 0:
return None
if self.current == len(self.history):
self.save(text)
self.current -= 1
text = self.history[self.current]
return text
def prev(self):
self.current += 1
if self.current >= len(self.history):
self.current = len(self.history)
return self.retrieve()
return self.history[self.current]
def reset(self):
self.current = len(self.history)
self.saved = None
def save(self, text):
self.saved = text
def retrieve(self):
return self.saved
def add(self, text):
if len(self.history) == 0 or text != self.history[len(self.history)-1]:
self.history.append(text)
self.reset()