#!/usr/bin/env python3 # # Nickname rating system # # The MIT License (MIT) # # Copyright © 2019 by luk3yx. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # import re # stolen from http://pi.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html letter_frequencies = { "E": 12.02, "T": 9.10, "A": 8.12, "O": 7.68, "I": 7.31, "N": 6.95, "S": 6.28, "R": 6.02, "H": 5.92, "D": 4.32, "L": 3.98, "U": 2.88, "C": 2.71, "M": 2.61, "F": 2.30, "Y": 2.11, "W": 2.09, "G": 2.03, "P": 1.82, "B": 1.49, "V": 1.11, "K": 0.69, "X": 0.17, "Q": 0.11, "J": 0.10, "Z": 0.07 } default_letter_frequency = 4 maximum_letter_frequency = 12 def letter_frequency(l): return letter_frequencies.get(l.upper(), default_letter_frequency) def inverse_letter_freqency(l): return maximum_letter_frequency - letter_frequency(l) def rate_string(nick): # Don't give a basic rating if the nickname is long if len(nick) > 20: return 0 # rate name based on letter frequency, divide by the length return sum(map(inverse_letter_freqency, nick.upper())) def rate_repetition(repetition_length, last_rate): if last_rate == 0 or repetition_length == 0: return 0 # squaring the rating to avoid penalizing common double letters like Billy, dividing by 64 to bring it back to earth m = (last_rate**2)/64 return repetition_length*m def repetition_rating(nick): nick = nick.lower() # look for repetition, remove points & remove repetitions for frequency analysis repetition_rating = 0 # we're catching repetitions between 1 and 4 chars in length # you can decrease this if you think it's looping too much max_rep_len = 4 nick_len = len(nick) # lets not let everyone do enormous loops if nick_len > 20: return 0 for c in range(1,max_rep_len+1): last_rate = 0 repetition_length = 0 # accumulator, lets us skip ahead in the loop a = 0 for N in range(nick_len): n = N+a nc = n+c ncc = nc+c if ncc > nick_len: repetition_rating += rate_repetition(repetition_length, last_rate) break this_set = nick[n:nc] next_set = nick[nc:ncc] if this_set == next_set: # rate the string here so we have it if we find the next one is not a repetition if repetition_length == 0: # only the first time, each repetition has the same rating... last_rate = rate_string(this_set) a += c - 1 repetition_length += 1 else: repetition_rating += rate_repetition(repetition_length, last_rate) repetition_length = 0 return -round(repetition_rating) def plural(n): return '' if n == 1 else 's' class BasePointModifier: __slots__ = ('text', 'points') def __repr__(self): return '<{} {}, {} point{}>'.format(type(self).__name__, repr(self.text), self.points, plural(self.points)) def get_raw_description(self): return repr(self.text) def get_description(self, raw_points): points = str(raw_points) if not points.startswith('-'): points = '+' + points return '{} point{}: {}'.format(points, plural(raw_points), self.get_raw_description()) def match(self, nick): raise NotImplementedError('match() not implemented') def get_points(self, nick): return self.points if self.match(nick) else 0 def __init__(self, text, points): self.text = str(text).lower() self.points = points class PointModifier(BasePointModifier): __slots__ = ('text', 'location') def get_raw_description(self): msg = repr(self.text) if self.location == 'start': msg = 'starts with ' + msg elif self.location == 'end': msg = 'ends with ' + msg else: msg = 'contains ' + msg return 'Nickname ' + msg + '.' def match(self, nick): nick = nick.lower() if self.location == 'start': return nick.startswith(self.text) elif self.location == 'end': return nick.endswith(self.text) return self.text in nick def __init__(self, text, points, location=None): super().__init__(text, points) self.location = location class RegexPointModifier(BasePointModifier): __slots__ = ('regex',) def match(self, nick): return bool(self.regex.search(nick)) def __init__(self, regex, points): super().__init__(regex, points) self.regex = re.compile(regex, flags=re.IGNORECASE) class CustomPointModifier(BasePointModifier): __slots__ = ('_func', '_lower') def match(self, nick): if self._lower: nick = nick.lower() return bool(self._func(nick)) def get_raw_description(self): return self.text def __init__(self, func, points, description='', lower=True): super().__init__('', points) self.text = description self._func = func self._lower = lower class DynamicPointModifier(CustomPointModifier): def get_points(self, nick): return self._func(nick) def match(self, nick): # return self.get_points(nick) != 0 raise NotImplementedError def __init__(self, func, text): super().__init__(func, 0, text) things = [ DynamicPointModifier(lambda n: round(rate_string(n)/len(n)), "Basic rating."), DynamicPointModifier(repetition_rating, "Repeating letters."), PointModifier('xX', -8, 'start'), PointModifier('Xx', -8, 'end'), PointModifier('pro', -8), PointModifier('hack', -8), PointModifier('hacker', -4), PointModifier('lol', -8), PointModifier('YT', -10), PointModifier('cute', -10), PointModifier('fortnite', -500), PointModifier('owo', -500), PointModifier('uwu', -500), # This is weirdly non-specific, who are you targeting here? RegexPointModifier('^[a-z]+[0-9]{3}(xx)?$', -5), CustomPointModifier(lambda n : len(n) < 4, -15, 'Insanely short nickname.', False), CustomPointModifier(lambda n : len(n) > 14, -8, 'Long nickname.', False), CustomPointModifier(lambda n : len(n) > 20, -15, 'Obnoxiously long nickname.', False), CustomPointModifier(lambda n : not n.isalnum(), -15, 'Non-alphanumeric characters used.', False), CustomPointModifier(lambda n : n == 'plebs', -9000, 'Sharing the nickname of a troll.'), CustomPointModifier(lambda n : n.isupper(), -8, 'Entirely uppercase nickname.', False), CustomPointModifier(lambda n : n in ('lurk', 'lurk3', 'lurk`', 'lurklite', 'father billy'), 9001, "Over 9000.") ] things.sort(key=lambda t : t.points) things = tuple(things) def rate_nick(nick): score = 0 lnick = nick.lower() reasons = [] for thing in things: points = thing.get_points(nick) if points != 0: score += points reasons.append(thing.get_description(points)) return score, reasons def main(): import argparse parser = argparse.ArgumentParser() parser.add_argument('nickname', help='The nickname to rate.') args = parser.parse_args() nick = args.nickname del parser, args score, reasons = rate_nick(nick) print('Score for nickname {}: {}'.format(repr(nick), score)) for reason in reasons: print(' •', reason) if __name__ == '__main__': main() elif 'register_command' in globals(): @register_command('nickrate') def nickrate_cmd(irc, hostmask, is_admin, args): r = hostmask[0] + ('' if hostmask[0].endswith('>') else ':') nick = args[-1].strip() if not nick or nick.startswith('<@'): return irc.msg(args[0], r + ' Invalid syntax! Syntax: .nickrate ') score, reasons = rate_nick(nick) msg = '{} Rating for nickname {}: \x02{} point{}.\x02'.format(r, repr(nick), score, plural(score)) if irc.msglen > 1024 and reasons: msg = msg + '\n • ' + '\n • '.join(reasons) irc.msg(args[0], msg)