#!/usr/bin/env python
##
##  jacus.py - JAbber Client of Utmost Simplicity
##             version 0.2
##
##  by Yusuke Shinyama, *public domain*
##
##  Prerequisite:
##      jabber.py - http://jabberpy.sourceforge.net/
##
##  Usage: $ ./jacus.py [options] username@server
##
##  Options:
##      -a logfile     : log file to append
##      -p pwdfile     : password file
##      -c encoding    : terminal encoding
##      -h host[:port] : server hostname (if different)
##      -S             : SSL mode
##      -C             : color mode
##      -B             : enable bell
##      -d             : debugging
##
##  Example:
##    $ ./jacus.py -S -C -a ~/log -h talk.google.com euske@gmail.com
##
##  Bugs:
##    * timeout.
##    * Ctrl-D terminates.
##

import sys, re, os, time, select
from jabber import jabber, xmlstream


ANSI_COLOR = {
  'bold':1, 'underline':4,
  'black':30, 'red':31, 'green':32, 'yellow':33, 'blue':34, 'magenta':35, 'cyan':36, 'white':37,
  '_black':40, '_red':41, '_green':42, '_yellow':43, '_blue':44, '_magenta':45, '_cyan':46, '_white':47,
  }
def ansi(col=0):
  return '\033[%sm' % str(ANSI_COLOR.get(col,''))
NAME_PAT = re.compile(r'[^@]+')
def shortname(name):
  return NAME_PAT.match(unicode(name)).group(0)


##  JACUS client
##
class JACUS:

  resource = 'JACUS%d' % os.getpid()
  keepalive = 300
  interval = 0.1
  
  def __init__(self, username, server, hostip=None,
               port=5223, logfp=None, ssl=False, pwdfile=None,
               encoding='euc-jp', color=True, bellok=False, debug=False):
    self.debug = debug
    self.server = server
    self.username = username
    self.password = None
    self.encoding = encoding
    self.color = color
    self.infp = sys.stdin
    self.outfp = sys.stdout
    self.lastseen = None
    self.bellok = bellok
    self.presence = (None, None)
    if pwdfile:
      fp = file(pwdfile)
      self.password = fp.read().rstrip()
      fp.close()
    self.logfp = logfp
    hostip = hostip or server
    if ssl:
      self.log('# Connecting: %s at %s:%d (SSL)...' % (server, hostip, port))
      self.write(green='# Connecting: %s at %s:%d (SSL)...' % (server, hostip, port))
      self.conn = jabber.Client(host=server, hostIP=hostip, port=port,
                                connection=xmlstream.TCP_SSL)
    else:
      self.log('# Connecting: %s at %s:%d...' % (server, hostip, port))
      self.write(green='# Connecting: %s at %s:%d...' % (server, hostip, port))
      self.conn = jabber.Client(host=server, hostIP=hostip, port=port)
    return

  def log(self, s):
    if self.logfp:
      (yy,mm,dd,HH,MM,SS,_d,_y,_z) = time.localtime()
      self.logfp.write('%02d-%02d-%02d %02d:%02d:%02d ' % (yy%100,mm,dd,HH,MM,SS))
      self.logfp.write(s.encode(self.encoding, 'replace'))
      self.logfp.write('\n')
      self.logfp.flush()
    return

  def write(self, s='', timestamp=False, bell=True, **labels):
    if timestamp:
      (yy,mm,dd,HH,MM,SS,_d,_y,_z) = time.localtime()
      if self.color:
        self.outfp.write(ansi('blue'))
      self.outfp.write('%02d:%02d ' % (HH,MM))
      if self.color:
        self.outfp.write(ansi())
    for (c,label) in labels.iteritems():
      if self.color:
        self.outfp.write(ansi(c))
      self.outfp.write(label.encode(self.encoding, 'replace'))
      if self.color:
        self.outfp.write(ansi())
    self.outfp.write(s.encode(self.encoding, 'replace'))
    if bell and self.bellok:
      self.outfp.write('\007')
    self.outfp.write('\n')
    self.outfp.flush()
    return

  def send_presence(self):
    (show, status) = self.presence
    show = show or 'available'
    if show.lower() == 'away':
      self.write(green=u'# Away (%s)' % status)
    else:
      self.write(green=u'# Available (%s)' % status)
    prs = jabber.Presence()
    prs.setShow(show)
    prs.setStatus(status or '')
    self.conn.send(prs)
    return

  def get_message(self, conn, msg):
    s = unicode(msg.getBody())
    if s:
      (yy,mm,dd,HH,MM,SS,_d,_y,_z) = time.localtime()
      w = shortname(msg.getFrom())
      self.log(u'%s> %s' % (w, s))
      self.write(s, cyan=w+u'> ', timestamp=True, bell=True)
      self.lastseen = str(msg.getFrom())
    return

  def get_presence(self, conn, prs):
    who = shortname(prs.getFrom())
    t = prs.getType() or 'available'

    # subscription request: 
    # - accept their subscription
    # - send request for subscription to their presence
    if t == 'subscribe':
      self.conn.send(jabber.Presence(to=who, type='subscribed'))
      self.conn.send(jabber.Presence(to=who, type='subscribe'))

    # unsubscription request: 
    # - accept their unsubscription
    # - send request for unsubscription to their presence
    elif t == 'unsubscribe':
      self.conn.send(jabber.Presence(to=who, type='unsubscribed'))
      self.conn.send(jabber.Presence(to=who, type='unsubscribe'))

    elif t == 'subscribed':
      self.write(magenta=u'# %s: subscribed' % who, timestamp=True)
    elif t == 'unsubscribed':
      self.write(magenta=u'# %s: unubscribed' % who, timestamp=True)

    elif t == 'available':
      show = str(prs.getShow() or 'available')
      self.write(magenta=u'# %s: online-%s (%s)' % (who, show, prs.getStatus()), timestamp=True)
      self.lastseen = str(prs.getFrom())
    elif t == 'unavailable':
      self.write(magenta=u'# %s: offline (%s)' % (who, prs.getStatus()), timestamp=True)
    return

  def get_iq(self, conn, iq):
    # I have no IQ!
    return

  def disconnected(self):
    self.log('! Disconnected.')
    self.write('Disconnected.', red='!!! ', timestamp=True)
    self.exit = 111
    return

  def select(self, whom):
    if whom:
      if '@' not in whom:
        whom = whom + '@' + self.server
      self.whom = whom
      self.write(green=u'# Selected: %s' % self.whom)
    return

  def do_command(self, line):
    if not line:
      self.write(green=u'# Help: "> whom" "/ available" "~ away" "."')

    elif line[0] == '.':
      self.conn.requestRoster()
      roster = self.conn.getRoster()
      self.write(green=u'# Roster')
      for jid in roster.getJIDs():
        (online, show, status) = (roster.getOnline(jid),
                                  roster.getShow(jid) or 'available',
                                  roster.getStatus(jid))
        if online.lower() == 'online':
          self.log(u'# Status: %s: online-%s (%s)' % (jid, show, status))
          self.write(bold=u'-- %s: online-%s (%s)' % (jid, show, status))
        else:
          self.log(u'# Status: %s: offline (%s)' % (jid, status))
          self.write(u'-- %s: offline (%s)' % (jid, status))

    elif line[0] == '>':
      self.select(line[1:].strip() or self.lastseen)

    elif line[0] == '/':
      self.presence = (None, line[1:].strip())
      self.send_presence()

    elif line[0] == '~':
      self.presence = ('away', line[1:].strip())
      self.send_presence()

    else:
      if not self.whom:
        self.select(self.lastseen)
      if self.whom:
        msg = jabber.Message(self.whom, line)
        msg.setType('chat')
        (yy,mm,dd,HH,MM,SS,_d,_y,_z) = time.localtime()
        self.log(u'<<< %s' % line)
        self.write(line, yellow='<<< ', timestamp=True)
        self.conn.send(msg)
      else:
        self.write(u'Not selected!', red='!!! ')
    return

  def run(self):
    self.conn.registerHandler('message', self.get_message)
    self.conn.registerHandler('presence', self.get_presence)
    self.conn.registerHandler('iq', self.get_iq)
    self.conn.setDisconnectHandler(self.disconnected)
    try:
      self.conn.connect()
    except IOError, e:
      self.log('! Error: %s' % e)
      self.write(u'Cannot connect: %s' % e, red='!!! ')
      return 1
    if not self.password:
      import getpass
      self.password = getpass.getpass('Password for %s: ' % self.username)
    if not self.conn.auth(self.username, self.password, self.resource):
      self.log('! Error: %s, %s' % (self.conn.lastErr, self.conn.lastErrCode))
      self.write(u'Auth failed: %s, %s' % (self.conn.lastErr, self.conn.lastErrCode))
      return 2
    self.log(u'# Logged in: %s' % self.username)
    self.write(green=u'# Logged in: %s' % self.username)
    self.conn.requestRoster()
    self.conn.sendInitPresence()
    self.exit = None
    self.whom = None
    t0 = time.time()
    poll = select.poll()
    poll.register(self.infp, select.POLLIN)
    while self.exit == None:
      t1 = time.time()
      if poll.poll(0):
        line = self.infp.readline()
        if not line:
          self.exit = 0
          break
        line = unicode(line, self.encoding, 'replace').strip()
        self.do_command(line)
        t0 = t1
      else:
        self.conn.process(self.interval)
      if t0 + self.keepalive < t1:
        self.send_presence()
        t0 = t1
    self.log('# Logged out')
    self.write(green=u'# Logged out')
    if self.logfp:
      self.logfp.write('\n')
    return self.exit


# main
def main(args):
  import getopt
  def usage():
    print ('usage: jacus.py [-d] [-a logfile] [-p pwdfile] [-c encoding] '
           '[-h host[:port]] [-B)ell] [-C)olor] [-S)sl] username@serverhost')
    sys.exit(2)
  try:
    (opts, args) = getopt.getopt(sys.argv[1:], 'da:p:c:h:CSB')
  except getopt.GetoptError:
    usage()
  (debug, logfp, pwdfile, encoding, hostip, port, color, ssl, bell) = \
          (False, None, None, 'euc-jp', None, 5223, False, False, False)
  for (k, v) in opts:
    if k == '-d': debug = True
    elif k == '-a': logfp = file(v, 'a')
    elif k == '-p': pwdfile = v
    elif k == '-c': encoding = v
    elif k == '-h':
      m = re.match(r'([^:]*)(:(\d+))?', v)
      if m:
        hostip = m.group(1)
        if m.group(2):
          port = int(m.group(3))
    elif k == '-C': color = True
    elif k == '-S': ssl = True
    elif k == '-B': bell = True
  if not args: usage()
  m = re.match(r'([^@]+)@(.+)', args[0])
  if not m: usage()
  (username, server) = (m.group(1), m.group(2))
  return JACUS(username, server,
               hostip=hostip, port=port, logfp=logfp, ssl=ssl, pwdfile=pwdfile,
               encoding=encoding, color=color, debug=debug).run()

#
if __name__ == '__main__': sys.exit(main(sys.argv[1:]))
