#!/usr/bin/env python from Tkinter import * import tkMessageBox from types import * import re, string, time, os, sys import pygale.engine if sys.platform != 'win32': pygale.engine.engine = pygale.engine.TkEngine() from pygale import * import thumbcache if sys.platform == 'win32': import win32api if sys.version >= '1.6': STRING_TYPES = [StringType, UnicodeType] else: STRING_TYPES = [StringType] # Set by ui.py Config = None # URL matching pattern URLPAT = 'https?:[\w/\.:;+\-~\%#?=&,()]+|www\.[\w/\.:;+\-~\%#?=&,()]+|' +\ 'ftp:[\w/\.:;+\-~\%#?=&,]+' # Debug memory leak DEBUG_MEMLEAK = 0 # Encapsulate information about a single connection to the Gale server class Connection: def __init__(self, name, sub): self._name = name # Name of tab self._sub = sub # Subscription self._master = None # Pointer to a GaleWin self._galec = None # Pointer to Gale client self._widget = None # Pointer to Text widget self._socketfd = -1 # Socket descriptor self._pufflist = [] # List of puffs self._curpuff = None # Currently selected puff (index into # pufflist) self._savepuffs = 0 # Save the puffs to disk self._logsent = 0 # Log sent puffs (0=no, 1=priv, 2=pub+priv) self._ccself = 0 # CC self on private puffs self._send_receipts = 1 # Send return receipts to private puffs # On new puff: self._beep = 0 # Beep self._command = None # Run command self._deiconify = 0 # Deiconify window self._highlight = 0 # Highlight tab self._lift = 0 # Lift tab # State vars self._temp_hash = {} # # For pickling # def __getstate__(self): # state = {} # saved_attribs = ['name', 'dosub', 'sub', 'beep', # 'command', 'deiconify', 'highlight', 'lift', 'savepuffs', # 'logsent'] # if self._savepuffs: # saved_attribs.append('pufflist') # for k in saved_attribs: # uname = '_' + k # state[uname] = self.__dict__[uname] # return state # # def __setstate__(self, dict): # self.__init__(dict['_name'], dict['_sub']) # for k in dict.keys(): # # Change old flash settings to new highlight settings # # Get rid of this when no one has an old config file anymore # if k == '_flash': # self.__dict__['_highlight'] = dict['_flash'] # else: # self.__dict__[k] = dict[k] # Accessors for config variables # Reconnect manually if necessary def __setitem__(self, key, val): uname = '_' + key self.__dict__[uname] = val # Getitem does double duty: using an integer index returns the item in # the pufflist, using a string index returns the config variable def __getitem__(self, key): if type(key) is IntType: return self._pufflist[key] else: uname = '_' + key return self.__dict__[uname] # Get our length as if we were the pufflist def __len__(self): return len(self._pufflist) # Accessors for other variables def widget(self): return self._widget def client(self): return self._galec # Store this master and widget into this connection def setwidget(self, master, widget): self._master = master self._widget = widget # Save original background color and color it pink (disconnected) self._origbg = self._widget.component('text').configure('bg')[-1] self._widget.component('text')['background'] = 'pink' def disconnect(self): "Request disconnection from server" self._galec.disconnect() # Make connection to server def connect(self): "Make connection to server" self.make_connection() if sys.platform == 'win32': self.check_socketread() # self._widget.configure(text_state = 'normal') # sub = string.strip(re.sub('\n|\r|\t', '', self._sub)) # self._widget.insert('end', 'Subscription is ' + sub + '\n') # self._widget.see('end') # self._widget.configure(text_state = 'disabled') # Our subscription changed, send it to the server def resubscribe(self): "Send a subscription request to server" sub = string.strip(re.sub('\n|\r|\t', ' ', self._sub)) sublist = string.split(sub, None) self._galec.sub_to(sublist, self.resubscribe2) def resubscribe2(self, bad_subs, good_subs): if bad_subs: d = tkMessageBox.showerror( title='Subscription error', parent=self._master, message= 'Unable to subscribe to: ' + string.join(bad_subs, ', ')) self._widget.configure(text_state = 'normal') if good_subs: self._widget.insert('end', 'Subscribed to %s\n' % string.join(good_subs, ' ')) else: self._widget.insert('end', 'Not subscribed\n') self._widget.see('end') self._widget.configure(text_state = 'disabled') def make_connection(self): "Set up connection to Gale server" if self._socketfd != -1: self.delete_filehandler() self._galec = pygale.GaleClient() self._galec.set_onconnect(self.connected) self._galec.set_ondisconnect(self.disconnected) self._galec.connect(lambda h, s=self: s.make_connection2(h)) def make_connection2(self, host): "Callback on failed connection" if host is None: # Couldn't connect to server self._master.set_status('Unable to connect to a gale server') # Retry after 2 seconds self._master.after(2000, self._galec.retry) return def connected(self, host): "Callback when connection established" self._socketfd = self._galec.socket() self.register_filehandler() self._widget.configure(text_state='normal') self._widget.insert('end', 'Connected to server at %s\n' % host) self._widget.configure(text_bg=self._origbg) self._widget.configure(text_state='disabled') self.resubscribe() def disconnected(self): "Callback when connection broken" self.delete_filehandler() self._widget.configure(text_state='normal') self._widget.insert('end', 'Disconnected from server\n') self._widget.configure(text_bg='pink') self._widget.configure(text_state='disabled') def register_filehandler(self): self._galec.set_puff_callback(lambda p, s=self: s._master.recvpuff(s, p)) self._galec.set_verify_callback(lambda p, s=self: s._master.verify_update(s, p)) # engine.set_timer_callback(self.timer_callback) def delete_filehandler(self): self._galec.del_puff_callback() # Handle timeout events # def timer_callback(self, *args): # next_timeout = engine.next_timeout() # if next_timeout is None: # return # time_til_timeout = next_timeout - time.time() # if (time_til_timeout <= 0): # engine.process() # return # self._master.after(int(time_til_timeout * 1000), # self.timer_callback) # ------------------------------------------------------------ # Icky workaround for Win32 def check_socketread(self, *args): engine.engine.process(0) self._master.after(500, self.check_socketread) # ------------------------------------------------------------ def pufflist(self): return self._pufflist def set_curpuff(self, index): if self._curpuff is not None: self._widget.tag_remove('selected-puff', '1.0', 'end') puff = self._pufflist[index] tag = 'pufftag-%i' % hash(puff) self._curpuff = index # Fix due to MKF: the tag ranges might be discontiguous ranges = self._widget.tag_ranges(tag) start = ranges[0] end = ranges[-1] self._widget.see(end) self._widget.see(start) self._widget.tag_add('selected-puff', start, end) def set_curpuff_frompuff(self, puff): self._master.set_loc_focus() index = self._pufflist.index(puff) self.set_curpuff(index) def curpuff(self): if self._curpuff is not None: return self._pufflist[self._curpuff] else: return None def curindex(self): return self._curpuff def addpuff(self, msg, new=1): self._widget.configure(text_state = 'normal') self._temp_hash[msg] = 1 self._pufflist.append(msg) tag = 'pufftag-%i' % hash(msg) signer_mark_begin = 'signer-mark-%i-begin' % hash(msg) # Indent margins self._widget.tag_config(tag, lmargin1=4, lmargin2=4, rmargin=4) # The previous line created the latest tag # Now raise the sel tag so that selections are always drawn on # top of any other coloration self._widget.tag_raise('quotation') self._widget.tag_raise('sel') # Check for this condition before adding all this text below if self._widget.dlineinfo('end - 2 lines'): scroll_bottom = 1 else: scroll_bottom = 0 list = msg.get_text('message/body') if not list: bodytext = '' else: bodytext = string.join(list, '|') bodytext = re.sub('\r\n', '\n', bodytext) if bodytext and bodytext[-1] != '\n': bodytext = bodytext + '\n' # Format return receipts specially if msg.get_text('answer.receipt') or\ msg.get_text('answer/receipt') or\ msg.get_text('notice/presence'): # Return receipts are never "new" if msg.get_text('answer.receipt') or\ msg.get_text('answer/receipt'): new = 0 now = msg.get_time_first('id/time', time.time()) self._widget.insert('end', '%s ' % time.strftime('%m-%d %H:%M:%S', time.localtime(now)), tag) if msg.get_text('answer.receipt'): self._widget.insert('end', 'received: ', tag) if msg.get_text('answer/receipt'): self._widget.insert('end', 'received: ', tag) if msg.get_text('notice/presence'): self._widget.insert('end', msg.get_text_first('notice/presence') + ': ', tag) self._widget.mark_set(signer_mark_begin, 'end - 1 chars') self._widget.mark_gravity(signer_mark_begin, 'left') self._widget.insert('end', msg.get_signer('*verifying*'), tag) if msg.get_text('message/sender'): self._widget.insert('end', " (%s)" % msg.get_text_first('message/sender'), tag) if msg.get_text('id/class'): self._widget.insert('end', ' %s' % msg.get_text_first('id/class'), (tag, 'small')) self._widget.insert('end', '\n', tag) # Format regular puff else: self._widget.insert('end', '\n', ('hrspacing', tag)) self._widget.insert('end', '\n', ('hr', tag)) self._widget.insert('end', '\n', ('hrspacing', tag)) sender_tags = (tag,) signer = msg.get_signer(None) if signer is None or (signer != '*unverified*' and\ signer != '*unsigned*'): # Display a thumbnail pic if Config.SHOW_THUMBS: thumb_mark = 'thumb-mark-%i' % hash(msg) self._widget.mark_set(thumb_mark, 'end -1 chars') self._widget.mark_gravity(thumb_mark, 'left') if signer: url = msg.get_text_first('message/image', None) if type(url) not in STRING_TYPES: url = None if url is None: url = thumbcache.id_to_url(signer) thumbcache.getthumb(url, lambda img, h=hash(msg), s=self: s.display_thumb(img, h), fromcache=not new) # Colorize sender attribute if signer: for key in Config.COLORS.keys(): if re.search(key, signer): sender_tags = sender_tags + ('%s-fg' % Config.COLORS[key],) break locs = string.split(msg.get_loc(), None) first = 1 if locs: self._widget.insert('end', 'To ', (tag, 'header')) for loc in locs: if first: first = 0 else: self._widget.insert('end', ', ', (tag, 'header')) domain, cats = pygale.parse_location_upper(loc) self._widget.insert('end', string.join(cats, '.'), (tag, 'bold')) self._widget.insert('end', '@' + domain, (tag, 'header')) if msg.get_text('message.keyword'): self._widget.insert('end', ' [%s]' % string.join(msg.get_text('message.keyword'), ', '), (tag, 'bold')) if msg.get_text('message/subject'): self._widget.insert('end', ' "%s"' % msg.get_text_first('message/subject'), (tag, 'bold')) if msg.get_text('id/class'): self._widget.insert('end', ' %s' % msg.get_text_first('id/class'), (tag, 'small')) msgstart = self._widget.index('end') self._widget.insert('end', '\n', tag) # Check for WQP in bodytext bodylines = string.split(bodytext, '\n') for i in range(len(bodylines)): line = bodylines[i] # Add the newline back to every line but the last if i != len(bodylines) - 1: line = line + '\n' if line[:2] == '> ': self._widget.insert('end', line, (tag, 'quotation')) else: self._widget.insert('end', line, tag) # Last line: -- sender, signer at date self._widget.insert('end', '-- ', (tag, 'right')) sender = msg.get_text_first('message/sender', None) if sender: self._widget.insert('end', sender, sender_tags + ('right',)) self._widget.insert('end', ' <', (tag, 'right')) self._widget.mark_set(signer_mark_begin, 'end -1 chars') self._widget.mark_gravity(signer_mark_begin, 'left') self._widget.insert('end', msg.get_signer('*verifying*'), (tag, 'right')) self._widget.insert('end', '>', (tag, 'right')) sentstr = '' if msg.get_time('id/time'): sentstr = ' at %s\n' %\ time.strftime("%m-%d %H:%M:%S", time.localtime(msg.get_time_first('id/time'))) else: sentstr = '\n' self._widget.insert('end', sentstr, (tag, 'right')) # Check for URLs (in non-receipt puffs only) urlpat = re.compile(URLPAT) rowoffset = 0 for line in string.split(bodytext, '\n'): coloffset = 0 while 1: match = urlpat.search(line, coloffset) if match: (patstart, patend) = match.span() while line[patend-1] in ['.', ',']: patend = patend - 1 coloffset = patend self._widget.tag_add('URL', '%s + %i lines + %i chars' % (msgstart, rowoffset, patstart), '%s + %i lines + %i chars' % (msgstart, rowoffset, patend)) else: break rowoffset = rowoffset + 1 # Set this to be the current puff on mouse click # This causes loads of problems with the other bindings self._widget.tag_bind(tag, '', lambda e, s=self, p=msg: s.set_curpuff_frompuff(p)) self._widget.tag_bind(tag, '', lambda e, s=self, p=msg: s.set_curpuff_frompuff(p)) # Delete old puffs from buffer if len(self._pufflist) > Config.SCROLLBACK: numdel = len(self._pufflist) - Config.SCROLLBACK to_be_deleted = self._pufflist[:numdel] # Compute start using the pufftag here, because the pufftags # will be deleted in the loop below first_remaining = self._pufflist[numdel] start = self._widget.tag_ranges( 'pufftag-%i' % hash(first_remaining))[0] self._pufflist = self._pufflist[numdel:] for p in to_be_deleted: signer_mark_begin = 'signer-mark-%i-begin' % hash(p) thumb_mark = 'thumb-mark-%i' % hash(p) self._widget.mark_unset(signer_mark_begin) self._widget.mark_unset(thumb_mark) pufftag = 'pufftag-%i' % hash(p) self._widget.tag_delete(pufftag) # also delete URL tags self._widget.tag_remove('URL', 1.0, start) self._widget.delete(1.0, start) if self._curpuff and self._curpuff >= numdel: self._curpuff = self._curpuff - numdel # Only scroll at end if end is visible # Warning: do this after setting the current puff, above, since # set_curpuff will cause the text widget to scroll there if scroll_bottom: self._widget.see('end') # retval = self._widget.tag_nextrange(tag, 1.0) # if retval: # self._widget.see(retval[1]) # self._widget.see(retval[0]) self._widget.configure(text_state = 'disabled') if not new: return # Beep if it's encrypted if self._beep: self._master.bell() if self._command is not None: # TODO: set envvars like for gsubrc # TODO: figure out how to fork() on Win32 if sys.platform != 'win32': args = string.split(self._command) args = [args[0]] + args pid = os.fork() if pid: # parent pass else: try: apply(os.execlp, tuple(args)) except: sys.exit(0) else: # This is rumored to work # win32api.WinExec(self._command, win32api.SW_SHOW) pass if self._lift: self._master.switch_screen(Config.SUBLIST.index(self)) if self._deiconify: self._master.master.deiconify() # Flash interacts weirdly with lifting the screen if self._highlight and not self._master.is_curscreen(self): index = Config.SUBLIST.index(self) self._master._buttonlist[index]['background'] = 'maroon' def display_thumb(self, thumb, hash): if thumb is not None: if not hasattr(self._widget, 'image_create'): self._master.gale_error('Unable to display thumbnail; '+ 'old version of Python/Tkinter?') return mark = 'thumb-mark-%i' % hash tag = 'pufftag-%i' % hash curstate = self._widget.component('text').configure('state')[-1] self._widget.component('text').configure(state='normal') try: index = self._widget.index(mark) self._widget.insert(index, ' ', tag) self._widget.image_create(index, image=thumb, align='bottom') except TclError, e: self._master.gale_error('Error displaying thumbnail: %s' % `e`) self._widget.component('text').configure(state=curstate) # Try to make the end of this puff visible if it's not # This may have weird effects ranges = self._widget.tag_ranges(tag) start = ranges[0] end = ranges[-1] if self._widget.dlineinfo(start): self._widget.see(end) # clear out unused thumbs in cache imagelist = [] for c in Config.SUBLIST: image_str = c.widget().image_names() l = string.split(image_str) l = map(lambda x: string.split(x, '#')[0], l) imagelist = imagelist + l thumbcache.GC(imagelist) def puff_verified(self, puff): signer = puff.get_signer(None) if signer is None: return signer_mark_begin = 'signer-mark-%i-begin' % hash(puff) self._widget.configure(text_state = 'normal') # Display a thumbnail pic thumb_mark = 'thumb-mark-%i' % hash(puff) if Config.SHOW_THUMBS and thumb_mark in self._widget.mark_names()\ and signer != '*unverified*' and signer != '*unsigned*': # Eeek, I don't like that *unverified* test url = thumbcache.id_to_url(signer) thumbcache.getthumb(url, lambda img, s=self, h=hash(puff): s.display_thumb(img, h)) # Redraw signer try: index = self._widget.index(signer_mark_begin) self._widget.delete(signer_mark_begin, '%s + 2 chars wordend wordend' % index) self._widget.insert(signer_mark_begin, signer) except TclError: pass self._widget.configure(text_state = 'disabled') def cancel_highlight(self): index = Config.SUBLIST.index(self) self._master._buttonlist[index]['background'] = self._origbg def clear_puffs(self): self._pufflist = [] self._widget.configure(text_state = 'normal') self._widget.delete(1.0, 'end') marks = self._widget.mark_names() for name in marks: self._widget.mark_unset(name) pufftags = self._widget.tag_names() prefixlen = len('pufftag') for name in pufftags: if name[:prefixlen] == 'pufftag': self._widget.tag_delete(name) self._widget.configure(text_state = 'disabled') self._curpuff = None