Skip to content

Commit

Permalink
Initial import of everything.
Browse files Browse the repository at this point in the history
  • Loading branch information
Tycho Andersen committed Jun 9, 2011
0 parents commit 2c5f036
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 0 deletions.
8 changes: 8 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
----------------------------------------------------------------------------
"THE BEER-WARE LICENSE" (Revision 42):
<tycho@tycho.ws> wrote this file. As long as you retain this notice you
can do whatever you want with this stuff. If we meet some day, and you think
this stuff is worth it, you can buy me a beer in return. Tycho Andersen
(Shamelessly stolen from: http://people.freebsd.org/~phk/)
----------------------------------------------------------------------------

20 changes: 20 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
tchat is an ncurses based interface to google voice. It uses
pygooglevoice (and thus polling and screen scraping) to interface
with google voice. It presents an interface similar to chat clients
(e.g. pidgin, mcabber), allowing you to "chat" via text messages.

It uses a password stored in python keyring. Specifically, it
requires that the password for the 'gmail' service and your supplied
username (currently a constant in tchat.py) be your password to
login. You can do this by booting up python:

tycho@mittens:~$ python
Python 2.6.5 (r265:79063, Apr 16 2010, 13:57:41)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import keyring
>>> keyring.set_password("gmail", "tycho@tycho.ws", "secret")

tchat displays only the most recent "conversation". It seems that
google voice resets these every 497 messages; tchat should seamlessly
move on to the next conversation, so you don't miss a beat.
6 changes: 6 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* make update not move the cursor
* make textbox always be in insert mode
* fix line break bug in sent messages
* html decode the messages before displaying them
* refactor _Textbox to overload do_command() instead of edit()

218 changes: 218 additions & 0 deletions tchat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/usr/bin/env python

"""
----------------------------------------------------------------------------
"THE BEER-WARE LICENSE" (Revision 42):
<tycho@tycho.ws> wrote this file. As long as you retain this notice you
can do whatever you want with this stuff. If we meet some day, and you think
this stuff is worth it, you can buy me a beer in return. Tycho Andersen
(Shamelessly stolen from: http://people.freebsd.org/~phk/)
----------------------------------------------------------------------------
"""

GOOGLE_VOICE_USERNAME = "tycho@tycho.ws"

import threading
import curses
import re
import BeautifulSoup
import keyring

from curses.textpad import Textbox

from googlevoice import Voice

class _Textbox(Textbox):
""" curses.textpad.Textbox requires users to ^g on completion, which is sort
of annoying for an interactive chat client such as this, which typically only
reuquires an enter. This subclass fixes this problem by signalling completion
on Enter as well as ^g. """
def __init__(*args, **kwargs):
Textbox.__init__(*args, **kwargs)

def edit(self, validate=None):
"""Edit in the window and collect the results. Results are given on Enter
as well as ^g"""
while 1:
ch = self.win.getch()
if validate:
ch = validate(ch)
if not ch:
continue
if not self.do_command(ch) or ch == 10: # break on enter
break
self.win.refresh()
return self.gather()

# Since curses is not thread safe and we do curses operations from multiple
# threads, we need to synchronize. This decorator does that.
def synchronized(lock_name):
""" Synchronization decorator. """

def wrap(f):
def new_function(*args, **kw):
self = args[0]
lock = getattr(self, lock_name)
lock.acquire()
try:
return f(*args, **kw)
finally:
lock.release()
return new_function
return wrap

class Chat(object):
def __init__(self):
self.curses_lock = threading.Lock()

global_screen = curses.initscr()
(globaly, globalx) = global_screen.getmaxyx()
curses.noecho()
self.chatscreen = curses.newwin(globaly-3, globalx, 0, 0)
self.entryscreen = curses.newwin(3, globalx, globaly-3, 0)
self.textpad = _Textbox(self.entryscreen)
self.textpad.stripspaces = True
self.history = []
self.update()

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
curses.nocbreak()
curses.echo()
curses.endwin()

@synchronized("curses_lock")
def update(self):
(cursory, cursorx) = curses.getsyx()
(rows, cols) = self.chatscreen.getmaxyx()

for (row, line) in zip(range(len(self.history)), self.history):
self.chatscreen.addstr(row, 0, line)
self.chatscreen.clrtoeol()

self.chatscreen.refresh()
curses.setsyx(cursory, cursorx)

def user_input(self):
cmd = self.textpad.edit()
self.entryscreen.clear()
return cmd.strip()

@synchronized("curses_lock")
def message(self, who, what):
(rows, cols) = self.chatscreen.getmaxyx()

def message_lines(message):
words = message.split()
accum = words[0]
words = words[1:]
while len(words) > 0:
while len(words) > 0 and len(accum) + len(words[0]) + 1 < cols:
accum += " " + words[0]
words = words[1:]
yield accum
accum = " "

for line in message_lines(who+': '+what):
self.history.append(line)

if len(self.history) > rows:
self.history = self.history[-rows:]

def get_status(self):
return self._status;

def set_status(self, status):
(rows, cols) = self.chatscreen.getmaxyx()

# trim the status to at most the number of columns
status = status[:cols]

status = property(get_status, set_status)

class GVChat(Chat):
def __init__(self, user, password):
Chat.__init__(self)

self.gv = Voice()
self.gv.login(user, password)
self.timer = None
self.to_phone = None
self.timedupdate(30)

def getsms(self):
""" Update the GVChat object with the first SMS thread in your
SMS box. """

# We could call voice.sms() directly, but I found this does a rather
# inefficient parse of things which pegs a CPU core and takes ~50 CPU
# seconds, while this takes no time at all.
data = self.gv.sms.datafunc()
data = re.search(r'<html><\!\[CDATA\[([^\]]*)', data, re.DOTALL).groups()[0]

divs = BeautifulSoup.SoupStrainer('div')
tree = BeautifulSoup.BeautifulSoup(data, parseOnlyThese=divs)

msgtype = str(tree.find("span", attrs={"class": "gc-message-type"}))
m = re.search('\((\d{3})\) (\d{3})-(\d{4})', msgtype)
self.to_phone = ''.join(m.groups())

self.smses = []
# we only want the first conversation
conversation = tree.find("div", attrs={"id" : True},recursive=False)
msgs = conversation.findAll(attrs={"class" : "gc-message-sms-row"})
for row in msgs:
msgitem = {"id" : conversation["id"]}
spans = row.findAll("span", attrs={"class" : True}, recursive=False)
for span in spans :
cl = span["class"].replace('gc-message-sms-', '')
msgitem[cl] = (" ".join(span.findAll(text=True))).strip()
self.smses.append(msgitem)

# Now that we have the SMSes, we can add their text and render them.
for sms in self.smses:
self.message(sms["from"][:-1], sms["text"])

def timedupdate(self, timeout):
self.getsms()
self.update()

# recycle the timedupdate
self.timer = threading.Timer(timeout, self.timedupdate, args=[timeout])
self.timer.start()

def __exit__(self, type, value, traceback):
self.timer.cancel()
self.gv.logout()
Chat.__exit__(self, type, value, traceback)

def sendsms(self, msg):
if not self.to_phone:
raise ValueError("No phone number :-(")
self.gv.send_sms(self.to_phone, msg)

def main():
passwd = keyring.get_password('gmail', GOOGLE_VOICE_USERNAME)
with GVChat(GOOGLE_VOICE_USERNAME, passwd) as chat:
while True:
chat.update()
cmd = chat.user_input()
if cmd == '/quit':
break
if cmd == '/refresh':
chat.getsms()
if not cmd.startswith('/'):
# Spawn a thread to handle sending the SMS and updating the chat
# screen. This way the UI doesn't block for users when google is being
# slow to respond :-)
def sms_sender_thread():
chat.sendsms(cmd)
chat.getsms()
chat.update()
t = threading.Thread(target=sms_sender_thread)
t.start()

if __name__ == "__main__":
main()

0 comments on commit 2c5f036

Please sign in to comment.