HarvardNow is an integrated texting service promoting information accessibility at Harvard. Get real time information about services and facilities, including shuttle schedules, laundry timers, and more (coming soon!). Text "demo" to (617) 925-6636 to try it out!
HarvardNow is a Python Flask App that runs on an HCS server. We use ngrok to create a url that we pass to a Twilio account. When the user texts the number, Twilio passes the text to our backend, where it gets process by app.py.
Python (2) - the programming language we're using. A python file ends in .py and can be run in the terminal with python filename.py. In this case, the server is running python app.py.
Flask - a framework that allows us to make an actual app out of app.py. When you text HarvardNow, Flask is reason we're able to do something in response, namely call the response function in app.py. We'll see more details when we look closely at the code.
HCS server - this is where the app gets run from. On this server (in a terminal), we run python app.py
ngrok - we run app.py on an HCS server, but we need it to be accessible from the outside world in order to call it when you text HarvardNow on your phone. That's where ngrok comes in - it gives us a URL for our server that's accessible from anywhere with an internet connection, which is important to our next point: Twilio
Twilio - connects your texting to our code (which is made accessible with ngrok). When you text HarvardNow, it's Twilio that finds out first. It then passes your text on to the URL we generated with ngrok.
So, all together now:
- You send a text to (617) 925-6636
- Twilio reads your message and sends the text to a URL generated by ngrok
- ngrok gives us access to the code (
app.py) running on the HCS server - Flask tells the server to call the
responsefunction - the python code (initially
app.pyand also all the other code that it calls inside that function) does the magic functional stuff and returns the response text you see - Twilio gets that message back and displays it as a response message
First, let's take a look at how services and commands are organized. Services are categories that you might want to get information about, such as 'laundry' and 'shuttle'. A command is a single function call that returns a specific piece of information, for example the status of all dryers in Weld Hall. Each service has its own folder in the services subdirectory.
Line 4 of app.py says to import everything (denoted as "*") from services. The __init__.py file in services tells python that services is a package that can be imported, and also specifies what to do when imported. In this case, importing from services should also import the laundry and shuttle subpackages. Each service is represented as a python subpackage in the services folder and contains its own __init__.py as well as a file named after that service (laundry.py for the laundry service). That file must contain two things:
- a variable called
special- this is a special string that should give the user a sense of what they can do with the service (see examples below) - a function
eval(cmd)that takes a command (we'll see how exactly we represent a commmand (cmd) in python) and returns a string containing the results of that evaluating that command
Everything else in a service (additional functions, helper files, etc.) is just there to satisfy one of these two requirements.
Now let's take another look at commands. A command is represented as a dictionary, meaning it has keys that are mapped to values. Remember that a command is just a function call. We think of a command in this case as an object that contains all the necessary information about its function call. This consists of three things:
service- a string representing the service this command is going to callargs- this is another dictionary containing all the arguments the function call will take as parameters. For example, if we want to ask thelaundryservice for the status of all dryers in Weld Hall, we're gonna want to specift that theroomidfor Weld Hall is 136259, themachinetypeis "dryer", and thelabelfor the command is "WELD HALL Dryers".tags- we use tags to uniquely identify a command with keywords.tagsis a list of these keywords.
We store all the available commands as a list of dictionaries in data.py in the root directory.
We're now ready to come back to app.py. The response() function processes the incoming text, checks that it doesn't contain a special keyword (or returns the appropriate special message if it does), determines what command(s) the user wants to run by filtering the master list of commands (box) with the words contained in incoming, and returns the result of evaluating those filtered commands. Both special and eval work by delegating to the appropriate service, since we know that every service has a special variable and a eval function (see code comments for details about the implementation of these functions).
Laundry's special function lists all the laundry rooms available to query. Data about each rooom is stored in a separate file within the laundry subpackage (data.py) and imported on Line 3 of laundry.py.
To evaluate a laundry command, get roomid and machinetype from the command's arguments and pass them to the getMachines function. getMachines uses urllib and urllib2 to open the website containing the laundry data and get the HTML, and beautifulsoup to parse that HTML and find its relevant parts. We then construct a list of machines, where a machine is represented as a dictionary specifying the machine's lr (laundry room id), id, name, and time (a string describing the status of the machine)
Laundry's special function lists all the shuttle stops and routes that the app knows about. This will give new users an idea of what they can find out more about.
A shuttle command comes in one of two types: stop and route since the user can ask about either of these. Each of these evaluations basically do the same thing: make an API call with the given arguments and returns them as a string. The code behind making an API call, located in and imported from api.py, simply gives the necessary information to the API and returns the restult in JSON format. API stands for Application Program Interface; it is a standard of communication between data sources (in this case, the location of the shuttle data) and the application that wants that data (in this case, our app). Python's requests library contains the functions necessary for making an API call. These are converted to JavaScript Object Notation (JSON) format, a standard for representing information as lists and objects (dictionaries).
Weather's special function simply gives the format that a user should input to receive weather information for a city.
The weather service works by doing a Google search on the indicated city. It then scrapes the data from the corresponding HTML.
- Python 2.7
- Python packages - install using
pip install packagenamein the terminal- flask
- twilio
- urllib
- urllib2
- bs4 (BeautifulSoup)
- requests
- datetime
- api
- Make a new folder under
services(we'll refer to it asmyservicehere) - In the
myservicefolder add a file called__init__.py. It doesn't need to contain anything in it - Also a add a python file in
myservicewith the same name as the folder (myservice.py)- It should contain a variable
specialthat stores a string. If a new user wants to use your service but isn't sure what to text, what message would you want to show them as an overview or demo? - It should also contain a function
eval(cmd)that takes a command as an argument and returns a string representing the result of evaluating that command. - Feel free to add any additional functions to help satisfy these requirements
- It should contain a variable
- In the
__init__.pyfile directly in theservicesfolder, add a line to import your main file from your folder:from myservice import myservice. This will make your service accessible fromapp.py - Add your service to the
evalfunction inapp.py. This function just uses a if-elif-else branch (conditional statement) to delegate a command to its proper service. Just above theelsestatement, add something like the following (we use'M'to be the shorthand string representation of your service. It's also'L'for laundry and'S'for shuttle):
elif cmd['service'] == 'M':
return myservice.eval(cmd['args'])
- Add your service to the
specialfunction inapp.py, aboveelif incoming.upper() == "DEMO"::
elif incoming.upper() == "MYSERVICE":
body = myservice.special
- Add your commands to
box(the master list of commands) in data.py. Be sure to give each aservice, any arguments your service needs to know as a dictionary (args), and a list oftagsthat uniquely identify the command:
{'service': 'M', 'args':{'info0':'data0', 'info1':'data1'}, 'tags': ['tag0','tag1','tag3']}