#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# Kigou v.20090116
# Storlek
# Public domain.
import pygtk
pygtk.require('2.0')
import gtk, gobject
import sys, re, cgi
from crypt import crypt
# --------------------------------------------------------------------------------------------------------------
# Pick one:
TRIP_CHAR = '!' # most English-language (and many international) boards
#TRIP_CHAR = u'\N{BLACK DIAMOND}' # most Japanese boards
#TRIP_CHAR = '~' # WTFux
#TRIP_CHAR = '#' # Futallaby
# --------------------------------------------------------------------------------------------------------------
# Tripcode implementations
# These functions take a unicode key value, and return the translated (key, salt) values that should be
# passed to crypt(). Note that the key is raw byte data, and may contain arbitrary high-bit characters.
# Some of these take advantage of the fact that crypt() returns a blank string if both the key and salt
# are blank, to indicate that the board would not use a tripcode for the given key value.
# (e.g.: blank key for 0ch, Shiichan, etc.; broken UTF-8 for Shiichan)
_NO_TRIPCODE = '', ''
# str.translate(_salt_table) is equivalent to s/[^\.-z]/\./g; tr/:;<=>?@[\\]^_`/A-Ga-f/;
# (and this is probably faster!)
_salt_table = (
'.............................................../0123456789ABCDEF'
'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefabcdefghijklmnopqrstuvwxyz.....'
'................................................................'
'................................................................'
)
# This is the only variable that code outside of this section needs to know about.
trippers = [] # (name, implementation)
# decorator for tripcode implementations
def tripper(name):
def wrap(implementation):
trippers.append((name, implementation))
return implementation
return wrap
@tripper('0ch')
def trip_0ch(key):
# No, I'm not kidding with all these replaces; this is actually what 0ch does.
key = (key.encode('sjis', 'xmlcharrefreplace')
.replace('\0', '') # does this ever actually change anything?
.replace('"', '"')
.replace('<', '<')
.replace('>', '>')
# s/\r\n|\r|\n/
/g
.replace('\r\n', '
')
.replace('\r', '
')
.replace('\n', '
')
.replace('\x81\x9A', '\x81\x99')
.replace('\x81\x9F', '\x81\x9E')
.replace('\x8D\xED\x8F\x9C', '\x81h\x8D\xED\x8F\x9C\x81h')
.replace('\x8A\xC7\x97\x9D', '\x81h\x8A\xC7\x97\x9D\x81h')
.replace('\x8A\xC7\x92\xBC', '\x81h\x8A\xC7\x92\xBC\x81h')
)
if not key:
return _NO_TRIPCODE
salt = (key + 'H.')[1:3].translate(_salt_table)
return key, salt
# Common implementation for Futaba and Futallaby
def _futaba_common(key, enc):
key = (key.encode(enc, 'xmlcharrefreplace')
.strip(' \t\n\r\0\x0b') # trim
.replace('"', '"') # htmlspecialchars (except & is changed back)
.replace('<', '<')
.replace('>', '>')
.replace(',', ',')
# and now for some major wtf'age: strtr($cap, ",", ",")
# which perhaps was meant to be str_replace, but isn't.
.replace('&', ',')
)
# XXX does futaba allow an empty key?
salt = (key + 'H.q')[1:3].translate(_salt_table)
return key, salt
@tripper('futaba')
def trip_futaba(key):
return _futaba_common(key, 'sjis')
# Same as Futaba, but assuming UTF-8. 4chan probably uses this same implementation...
@tripper('futallaby')
def trip_futallaby(key):
return _futaba_common(key, 'utf8')
# Shiichan rigidly enforces proper UTF-8 and refuses to make a tripcode if the data is broken.
# Also, it doesn't allow a blank trip.
@tripper('shiichan')
def trip_shiichan(key):
key = (key.encode('utf8', 'ignore')
.replace('&', '&') # htmlspecialchars, with ENT_QUOTES
.replace('"', '"')
.replace('\'', ''')
.replace('<', '<')
.replace('>', '>')
)
if not key:
return _NO_TRIPCODE
salt = (key + 'H.q')[1:3].translate(_salt_table)
return key, salt
# Wakaba is ... thorough.
_re_waha_decodestr = re.compile(r'(?:([0-9]*)|([Xx&])([0-9A-Fa-f]*))([;&])')
_re_waha_cleanstr = re.compile(r'&(#([0-9]+);|#x([0-9A-Fa-f]+);|)')
_re_waha_stripctrl = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]+')
@tripper('wakaba')
def trip_wakaba(key):
def dec_or_hex(d, h):
try:
return (int(d) if d else int(h, 16))
except:
return 0
def forbidden(o):
return o > 1114111 or o < 32 or 0xd800 <= o <= 0xdfff or 0x202a <= o <= 0x202e
def decode_string(m):
d, xamp, h, end = m.groups()
o = dec_or_hex(d, h)
if '&' in (end, xamp):
return m.group(0)
elif forbidden(o):
return ''
elif o in (35, 38):
return m.group(0)
else:
return unichr(o)
def clean_string(m):
g, d, h = m.groups()
if not g:
return '&'
elif forbidden(dec_or_hex(d, h)):
return ''
else:
return m.group(0)
key = _re_waha_decodestr.sub(decode_string, key)
key = key.encode('sjis', 'xmlcharrefreplace')
key = (_re_waha_cleanstr.sub(clean_string, key)
.replace('<', '<')
.replace('>', '>')
.replace('"', '"')
.replace('\'', ''')
.replace(',', ',')
)
key = _re_waha_stripctrl.sub('', key)
salt = (key + 'H..')[1:3].translate(_salt_table)
return key, salt
# Thorn is possibly the most straightforward implementation of them all.
# Too bad various other boards set the precedent long beforehand and Thorn was never really that popular,
# because this is nice and simple... minus the fact that SJIS is arguably a better encoding for use with
# crypt() merely because its keyspace per byte is larger and more tightly packed. Oh well.
# Drydock hasn't changed this.
@tripper('thorn')
def trip_thorn(key):
key = key.encode('utf8', 'xmlcharrefreplace') # ?
salt = (key + 'H..')[1:3].translate(_salt_table)
return key, salt
# ... and then on the other hand, there's this nonsense.
# (this regex is technically wrong, but doing php_strip_tags "properly" would be insane)
_re_htmltag = re.compile(r'''<(?:[^'">]*(?:'[^'>]*'|"[^">]*"))*[^'">]*>''')
# XXX blank trip?
@tripper('trevorchan07')
def trip_trevorchan(key):
key = key.encode('utf8', 'xmlcharrefreplace')
key = _re_htmltag.sub('', key) # strip_tags
key = (key
# addslashes
.replace('\\', '\\\\')
.replace('\0', '\\\0')
.replace('\'', '\\\'')
.replace('"', '\\\"')
# retarded copy-and-paste from futaba's incorrect use of strtr()
.replace('&', ',')
)
salt = (key + 'H.q')[1:3].translate(_salt_table)
return key, salt
# This is actually taken from Kusaba-X and Serissa, because I can't find the original Kusaba source anywhere.
# TODO: look at Trevorchan 0.9 and Kusaba proper, and see if any of them have changed things
# XXX blank trip?
@tripper('kusaba')
def trip_kusaba(key):
try: enc = key.encode('sjis')
except: enc = key.encode('utf8', 'xmlcharrefreplace')
key = (enc
# Well, it got rid of the addslashes nonsense, but that cargo-cult strtr() is still there,
# and to top it off, they added a space after the comma for whatever reason.
.replace('&', ',')
.replace('#', ' ')
)
salt = (key + 'H.q')[1:3].translate(_salt_table)
return key, salt
# Another Futaba knockoff, seems fairly obscure.
# XXX blank trip?
_re_pixmi_xml = re.compile(u'[\x01-\x08\x0B-\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFDD0-\uFDDF]+')
@tripper('pixmicat')
def trip_pixmicat(key):
key = _re_pixmi_xml.sub('', key) # from CleanStr
key = (key
# trim
.strip(' \t\n\r\0\x0b')
# htmlspecialchars
.replace('"', '"')
.replace('<', '<')
.replace('>', '>')
# XXX trip prefix replacement is language dependent, so only one of these two should be done
.replace('!', '|') # trip prefix for en_US
.replace(u'\N{BLACK DIAMOND}', u'\N{WHITE DIAMOND}') # other languages
# Strange bug, CAP_SUFFIX is hardcoded and contains a leading space, but the replacement text
# still uses the language's config as it should. Go figure.
# Also, the en_US configuration uses an o-slash for the replacement char here.
.replace(u' \N{BLACK STAR}', u'\N{WHITE STAR}')
# converting charset *last* due to weird character replacements
.encode('utf8', 'xmlcharrefreplace')
)
salt = (key + 'H.q')[1:3].translate(_salt_table)
return key, salt
# this code appears to have originated on /soc/.
@tripper('pyib')
def trip_pyib(key):
import string
try:
key = key.encode('sjis')
except:
return _NO_TRIPCODE
key = (key
.replace('"', '"')
.replace('<', '<')
.replace('>', '>')
)
if not key:
return _NO_TRIPCODE
salt = (key + 'H..')[1:3].translate(_salt_table)
return key, salt
# (yes, this is directly from the source code. -- Storlek)
@tripper('matsuba')
def trip_matsuba(key):
key = (key.encode('sjis', 'xmlcharrefreplace')
.replace('"', '"')
.replace('<', '<')
.replace('>', '>')
)
salt = (key + 'H..')[1:3].translate(_salt_table)
return key, salt
# --------------------------------------------------------------------------------------------------------------
class TripList(gtk.TreeView):
def __init__(self):
gtk.TreeView.__init__(self)
# set up the model
store = gtk.TreeStore(
gobject.TYPE_STRING, # search string (key / trip without leading character)
gobject.TYPE_STRING, # visible key/trip, with # or whatever preceding it
gobject.TYPE_STRING, # applicable boards
gobject.TYPE_STRING, # crypt() values (tooltip)
)
self.set_model(store)
# set up the view columns
cr = gtk.CellRendererText()
tvc = gtk.TreeViewColumn('Tripcode', cr, text=1)
self.append_column(tvc)
tvc = gtk.TreeViewColumn('Boards', cr, text=2)
tvc.set_expand(True)
self.append_column(tvc)
self.set_enable_search(False)
self.set_headers_clickable(False)
self.set_headers_visible(False)
self.set_reorderable(False)
self.set_search_column(0) # search for keys or trips
self.set_tooltip_column(3)
# --------------------------------------------------------------------------------------------------------------
def drepr(s):
return '"' + s.encode('string_escape').replace('"', '\\"') + '"'
def add_tripcode(entry, triplist):
key = entry.get_text()
# urgh
key = key.decode('utf8')
results = {} # results[trip]: [(impl, bkey, salt), ...]
for name, impl in trippers:
bkey, salt = impl(key)
trip = crypt(bkey, salt)[3:]
bkey, salt = bkey[:8], salt[:2]
results.setdefault(trip, []).append((name, bkey, salt))
results = sorted(results.items(), key=lambda(k,v): -len(v))
if len(results) == 1:
trip, impls = results[0]
_, bkey, salt = impls[0]
# no sense being redundant.
results = [(trip, [('(all)', bkey, salt)])]
# now add 'em
store = triplist.get_model()
last = i = store.prepend(None, [key, '#' + key, None, None])
for trip, impls in results:
_, bkey, salt = impls[0]
store.append(i, [
trip,
(TRIP_CHAR + trip if trip else ''),
', '.join(name for name, bkey, salt in impls),
cgi.escape('crypt(%s, %s)' % (drepr(bkey), drepr(salt)))
])
triplist.expand_row(store.get_path(i), True)
gobject.idle_add(triplist.get_parent().get_vadjustment().set_value, 0)
entry.select_region(0, len(key))
entry.grab_focus()
def add_button_clicked(btn, entry, triplist):
add_tripcode(entry, triplist)
def entry_activate(entry, triplist):
add_tripcode(entry, triplist)
# --------------------------------------------------------------------------------------------------------------
# MAIN SCREEN TURN ON
def main():
w = gtk.Window()
w.set_title('Kigou')
w.set_default_size(width=500, height=500)
w.connect('destroy', gtk.main_quit)
sw = gtk.ScrolledWindow()
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
sw.set_shadow_type(gtk.SHADOW_IN)
triplist = TripList()
sw.add(triplist)
h = gtk.HBox()
entry = gtk.Entry()
entry.connect('activate', entry_activate, triplist)
button = gtk.Button('Check trip')
button.connect('clicked', add_button_clicked, entry, triplist)
h.pack_start(gtk.Label('#'), expand=False, fill=False)
h.pack_start(entry)
h.pack_start(button, expand=False, fill=False)
v = gtk.VBox()
v.pack_start(sw)
v.pack_start(h, expand=False, fill=False)
v.set_spacing(4)
w.add(v)
w.set_border_width(8)
w.show_all()
entry.grab_focus()
gtk.main()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass