Friday, October 9, 2009

Python - Custom Google Voice API - Installable Module

Google Voice is a great service, but it lacks a couple of important things:
  1. An official API from Google
  2. A way to send mass SMS messages
I have spend some time creating my own unofficial API that is, I hope, easy to use and extend. I am by no means a seasoned Python developer, but I know enough to get what I want done. Many of the posts on my blog that interact with Google Voice depend on a "gvoice.py" script that I wrote, which contains several helpful classes. These class allow you to perform some basic actions, such as:

  1. Logging in to your Google Voice Account
  2. Sending a text message
  3. Placing a phone call

The script also allows you to:
  1. Download your Google Contacts into memory (Used mainly to get contact information to call or send a message)
  2. Programmatically select from you Contact Groups which people to contact
  3. Retrieve the numbers you have set up to work with your Google Voice account

Currently the script has only been tested using Python 2.6 (Python 3 might work, but I am not sure).

You can download the module with installation instructions below. Or, just scroll down to the bottom of the post to see the source in it's entirety.


Here are some of the posts where I show examples on how to use this module:

And the complete source:


"""
gvoice.py

Created by: Scott Hillman

http://www.everydayscripting.blogspot.com

This module comes as is an with no warranty.
You are free to use, modify and distribute this
code however you wish, but I ask that if you post
it anywhere, you at least make reference to me and
my blog where you acquired it.
"""

import csv
import sys
import re
import urllib
import urllib2

class GoogleVoiceLogin:
"""
Class that attempts to log in the Google Voice using the provided
credentials.

If either no password or email is provided, the user will be
prompted for them.

Once instantiated, you can check to see the status of the log in
request by accessing the "logged_in" attribute

The primary usage of a GoogleVoiceLogin object is to be passed
in to other constructors, such as the TextSender, or NumberDialer
"""

def __init__(self, email = None, password = None):
"""
Given the email and password values, this method will attempt to log
in to Google Voice. The "response" attribute can be checked to
see if the login was a success or not.

If the login was successful, the "opener" and "key" attributes will
be available to use when creating other objects.

To use an this object with the other classes in this module, simply
pass it in to the constructor. (ie text_sender = TextSender(gv_login))
"""

if email is None:
email = raw_input("Please enter your Google Account username: ")
if password is None:
import getpass
password = getpass.getpass("Please enter your Google Account password: ")

# Set up our opener
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
urllib2.install_opener(self.opener)

# Define URLs
self.login_page_url = 'https://www.google.com/accounts/ServiceLogin'
self.authenticate_url = 'https://www.google.com/accounts/ServiceLoginAuth'
self.gv_home_page_url = 'https://www.google.com/voice/#inbox'

# Load sign in page
login_page_contents = self.opener.open(self.login_page_url).read()

# Find GALX value
galx_match_obj = re.search(r'name="GALX"\s*value="([^"]+)"', login_page_contents, re.IGNORECASE)

galx_value = galx_match_obj.group(1) if galx_match_obj.group(1) is not None else ''

# Set up login credentials
login_params = urllib.urlencode({
'Email' : email,
'Passwd' : password,
'continue' : 'https://www.google.com/voice/account/signin',
'GALX': galx_value
})

# Login
self.opener.open(self.authenticate_url, login_params)

# Open GV home page
gv_home_page_contents = self.opener.open(self.gv_home_page_url).read()

# Fine _rnr_se value
key = re.search('name="_rnr_se".*?value="(.*?)"', gv_home_page_contents)

if not key:
self.logged_in = False
else:
self.logged_in = True
self.key = key.group(1)

class ContactLoader():
"""
This class is used to download and organize a csv file
of Google Contacts.It is often used in conjunction with
the ContactSelector class.

Example:

contact_loader = ContactLoader(gv_login)
contact_selector = ContactSelector(contact_loader)
"""
def __init__(self, gv_login):
"""
Pass in a GoogleVoiceLogin object, and the persons Google Contacts
Will be downloaded and organized into a structure called
"contacts_by_group_list"
which is organized in form:

[(1, ('group_name', [contact_list])), (2, ('group_name', [contact_list]))]

Which allows for easy access to any group.
"""
self.opener = gv_login.opener
self.contacts_csv_url = "http://mail.google.com/mail/contacts/data/export"
self.contacts_csv_url += "?groupToExport=^Mine&exportType=ALL&out=OUTLOOK_CSV"

# Load ALL Google Contacts into csv dictionary
self.contacts = csv.DictReader(self.opener.open(self.contacts_csv_url))

# Create dictionary to store contacts and groups in an easier format
self.contact_group = {}
# Assigned each person to a group that we can get at later
for row in self.contacts:
if row['First Name'] != '':
for category in row['Categories'].split(';'):
if category == '':
category = 'Ungrouped'
if category not in self.contact_group:
self.contact_group[category] = [Contact(row)]
else:
self.contact_group[category].append(Contact(row))

# Load contacts into a list of tuples...
# [(1, ('group_name', [contact_list])), (2, ('group_name', [contact_list]))]
self.contacts_by_group_list = [(id + 1, group_contact_item)
for id, group_contact_item in enumerate(self.contact_group.items())]

class Contact():
"""
Simple class to contain information on each Google Contact person.

Only stores information on:
First Name
Last Name
Mobile Number
Email address
"""
def __init__(self, contact_detail):
"""
Extract data from the given contact_detail

The following attributes are available:

first_name
last_name
mobile
email
"""
self.first_name = contact_detail['First Name'].strip()
self.last_name = contact_detail['Last Name'].strip()
self.mobile = contact_detail['Mobile Phone'].strip()
self.email = contact_detail['E-mail Address'].strip()

def __str__(self):
return self.first_name + ' ' + self.last_name

# Class to assist in selected contacts by groups
class ContactSelector():
"""
Class with helps select contacts after using the ContactLoader
object to download them.

Provides methods to:
1) Display the list of groups (get_group_list())
2) Set the selected group to work with (set_selected_group(group_id))
3) Get the contacts from the working list (get_contacts_list())
4) Remove names from the working list(remove_from_contact_list(contacts_to_remove_list))
"""
def __init__(self, contact_loader):
"""
Initialize the object - a ContactLoader object is expected here
"""
self.contacts_by_group_list = contact_loader.contacts_by_group_list
self.contact_list = None

def get_group_list(self):
"""
Extract a list of all the groups.
List is in the form:
[(1, 'Group Name'), (2, 'Groups Name'), ...]
"""
return [(item[0], item[1][0]) for item in self.contacts_by_group_list]

def set_selected_group(self, group_id):
"""
Select the group to work with.

This method will make the working contact_list contain all the
contacts from the selected group.
"""
self.contact_list = self.contacts_by_group_list[group_id - 1][1][1]

# Return the contact list so far
def get_contacts_list(self):
"""
Return a list of all the contacts, and assign them each a number

List is in the form:
[(1, Contact), (2, Contact), ...]
"""
return [(id + 1, contact) for id, contact in enumerate(self.contact_list)]

def remove_from_contact_list(self, contacts_to_remove_list):
"""
Accepts a one based list of ids, indicating which contacts
to remove from the list.

List needs to be a list in ints:
[3, 6, 7]
"""
if self.contact_list is None:
return
for id in contacts_to_remove_list:
if id in range(0, len(self.contact_list) + 1):
self.contact_list[id - 1] = None
self.contact_list = [contact for contact in self.contact_list if contact is not None]

class NumberRetriever():
"""
Class that will allow you to retrieve all stored phone numbers and their aliases
"""

def __init__(self, gv_login):
"""
Pass in the GoogleVoiceLogin object, this class will then
download all the numbers and aliases of the persons GV Account
"""
self.opener = gv_login.opener
self.phone_numbers_url = 'https://www.google.com/voice/settings/tab/phones'
phone_numbers_page_content = self.opener.open(self.phone_numbers_url).read()

# Build list of all numbers and their aliases
self.phone_number_items = [(match.group(1), match.group(2))
for match
in re.finditer('"name":"([^"]+)","phoneNumber":"([^"]+)"',
phone_numbers_page_content)]

def get_phone_numbers(self):
"""
Return the list of phone numbers in the form:
[(1, number), (2, number)...]
"""
return [(id + 1, (phone_number_item))
for id, phone_number_item
in enumerate(self.phone_number_items)]

class TextSender():
"""
Class used to send text messages.

Example usage:

gv_login = GoogleVoiceLogin('username', 'password')
text_sender = TextSender(gv_login)
text_sender.text = "This is an example"
text_sender.send_text('555-555-5555')

if text_sender.response:
print "Success!"
else:
print "Fail!"
"""
def __init__(self, gv_login):
"""
Pass in a GoogleVoiceLogin object, set the text message
and then call send_text
"""
self.opener = gv_login.opener
self.key = gv_login.key
self.sms_url = 'https://www.google.com/voice/sms/send/'
self.text = ''

def send_text(self, phone_number):
"""
Sends a text message containing self.text to phone_number
"""
sms_params = urllib.urlencode({
'_rnr_se': self.key,
'phoneNumber': phone_number,
'text': self.text
})
# Send the text, display status message
self.response = "true" in self.opener.open(self.sms_url, sms_params).read()

class NumberDialer():
"""
Class used to make phone calls.

Example usage:

gv_login = GoogleVoiceLogin('username', 'password')
number_dialer = NumberDialer(gv_login)
number_dialer.forwarding_number = 'number-to-call-you-at'

number_dialer.place_call('number-to-call')

if number_dialer.response:
print "Success!"
else:
print "Fail!"
"""
def __init__(self, gv_login):
self.opener = gv_login.opener
self.key = gv_login.key
self.call_url = 'https://www.google.com/voice/call/connect/'
self.forwarding_number = None

def place_call(self, number):
"""
Pass in a GoogleVoiceLogin object, set the forwarding_number
and then call place_call('number-to-call')
"""
call_params = urllib.urlencode({
'outgoingNumber' : number,
'forwardingNumber' : self.forwarding_number,
'subscriberNumber' : 'undefined',
'remember' : '0',
'_rnr_se': self.key
})

# Send the text, display status message
self.response = self.opener.open(self.call_url, call_params).read()

Thursday, October 8, 2009

Python - Google Voice from the command line (CLI SMS and Calling)

I have written about Google Voice a few times already, but the script that gets the most attention is the one that allows you to mass text/call people in your Google Contacts Groups, which you can find here.

Many people do not need such a large script, since they simply want to be able to use something to send one text message at a time through Google Voice at the command line. Even though my scripts were written with mass contacting in mind, you can still use my gvoice module to easily contact just one person.

To use this script, you will need to download my gvoice module. There are instructions about installing this module here, as well as some documentation on it.

Once you have the gvoice module installed, creating a command line script to text someone is trivial:


import gvoice
import sys

if len(sys.argv) < 3:
print "You must provide both a number and a message!"
exit()

gv_login = gvoice.GoogleVoiceLogin('username', 'password')
text_sender = gvoice.TextSender(gv_login)

text_sender.text = sys.argv[2]
text_sender.send_text(sys.argv[1])

if text_sender.response:
print 'Success! Message sent!'
else:
print 'Failure! Message not sent!'


Notice in this script I am explicitly setting the username and password. If you leave out those parameters, you will be prompted for them at run time.

You can now call the script (which I am naming"smser.py") like so:

python smser.py "555-555-5555" "This was sent from the command line!"

If you want to make a call to someone, it is just as easy:


import gvoice
import sys

if len(sys.argv) < 3:
print "You must provide both a forwarding number and number to dial!"
exit()

gv_login = gvoice.GoogleVoiceLogin('username', 'password')
number_dialer = gvoice.NumberDialer(gv_login)
number_dialer.forwarding_number = sys.argv[2]
number_dialer.place_call(sys.argv[1])

if number_dialer.response:
print 'Success! You should hear the phone ringing shortly...'
else:
print 'Call failed!'


You would use this script (which I am naming "caller.py") much the same way:

python caller.py "number-to-dial" "number-to-call-you-at"


Just remember the order of the numbers there, and you should be good to go.

Why would this CLI be helpful to you if you have the nice GUI of Google Voice? I can think of at least two good reasons:

1) It is faster to do at the command line (If you know the numbers!)
2) You can automate the sending of a text message. One reader shared with me the idea of executing something at the command line that would take a long time, and chaining on the the end of it "smser.py 'my-number' 'Finished!'", so he could walk away from his computer and know when the command had finished up. (Thanks Shane!) You could also set up cron jobs to send out a message on a given day and time (Another friend used this to wish himself Happy Birthday)

If you are on a Windows machine, you can add the folder where you download these scripts to your PATH environment variable so you can access them anywhere. If you want to go one step further and make it so you don't even have to type in "python" before the file name or ".py" after the file name when executing a Python script, follow the instructions here.

If you are in Linux, you should be able to create a symbolic link in /usr/bin if you want to achieve the same thing.

As usual, here is a zip file containing the source code of the examples shown above, plus the examples shown in the respective files. I also threw in there the installable gvoice module and the standalone gvoice module with some brief instructions on how to install either.

gv_cli_examples.zip

Let me know if you have any questions or problems using these scripts.

Friday, October 2, 2009

Python - Fixes to the Google Login script

Just recently Google changed some things behind the scenes to their login page. More specifically, on the login page there is now a hidden form element called "GALX", the value of which must be passed along with the Username and Password when signing in - other wise, the sign in fails. This affected a few of my scripts, most notably my Google Voice Mass Contact script which a few visitors let me know about.

I modified the scripts that are contained in that post to work with the new system. So, if you have been having trouble with the mass contact script, go to that post and re-download the scripts.

Here is the complete login script, including how to get the _rnr_se value after logging in.


import urllib
import urllib2
import getpass
import re

email = raw_input("Enter your Google username: ")
password = getpass.getpass("Enter your password: ")

opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())
urllib2.install_opener(opener)

# Define URLs
loing_page_url = 'https://www.google.com/accounts/ServiceLogin'
authenticate_url = 'https://www.google.com/accounts/ServiceLoginAuth'
gv_home_page_url = 'https://www.google.com/voice/#inbox'

# Load sign in page
login_page_contents = opener.open(loing_page_url).read()

# Find GALX value
galx_match_obj = re.search(r'name="GALX"\s*value="([^"]+)"', login_page_contents, re.IGNORECASE)

galx_value = galx_match_obj.group(1) if galx_match_obj.group(1) is not None else ''

# Set up login credentials
login_params = urllib.urlencode( {
'Email' : email,
'Passwd' : password,
'continue' : 'https://www.google.com/voice/account/signin',
'GALX': galx_value
})

# Login
opener.open(authenticate_url, login_params)

# Open GV home page
gv_home_page_contents = opener.open(gv_home_page_url).read()

# Fine _rnr_se value
key = re.search('name="_rnr_se".*?value="(.*?)"', gv_home_page_contents)

if not key:
logged_in = False
print 'Failed!'
else:
logged_in = True
key = key.group(1)
print 'Success!'