This commit is contained in:
Jacob Henry 2016-10-23 18:05:09 +00:00
commit 0121086306
9 changed files with 528 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
apitoken.txt
killswitch.txt
client_secret.json
*.pyc

11
CallOfTheVoid.py Normal file
View File

@ -0,0 +1,11 @@
from slackclient import SlackClient
#One last call from beyond the grave
apifile = open("apitoken.txt", 'r')
SLACK_API = next(apifile)
apifile.close();
slack = SlackClient(SLACK_API)
slack.api_call("chat.postMessage", channel="@jacobhenry", text="Alas poor yorrick. I died!")

140
GoogleApi.py Executable file
View File

@ -0,0 +1,140 @@
"""
Examples provided by google for using their api.
Very slightly modified by me to easily just get credentials
"""
import httplib2
import os
from apiclient import discovery
import oauth2client
from oauth2client import client
from oauth2client import tools
try:
import argparse
flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args(["--noauth_local_webserver"])
except ImportError:
flags = None
# If modifying these scopes, delete your previously saved credentials
# at ~/.credentials/sheets.googleapis.com-python-quickstart.json
SCOPES = 'https://www.googleapis.com/auth/spreadsheets.readonly'
CLIENT_SECRET_FILE = 'client_secret.json'
APPLICATION_NAME = 'SlickSlacker'
def get_sheets_service(credentials):
http = credentials.authorize(httplib2.Http())
discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?'
'version=v4')
service = discovery.build('sheets', 'v4', http=http, discoveryServiceUrl=discoveryUrl)
return service
def get_sheets_credentials():
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
Returns:
Credentials, the obtained credential.
"""
home_dir = os.path.expanduser('~')
credential_dir = os.path.join(home_dir, '.credentials')
if not os.path.exists(credential_dir):
os.makedirs(credential_dir)
credential_path = os.path.join(credential_dir, 'sheets.googleapis.com-python-quickstart.json')
store = oauth2client.file.Storage(credential_path)
credentials = store.get()
if not credentials or credentials.invalid:
flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
flow.user_agent = APPLICATION_NAME
if flags:
credentials = tools.run_flow(flow, store, flags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
print('Storing credentials to ' + credential_path)
return credentials
def get_calendar_credentials():
"""Gets valid user credentials from storage.
If nothing has been stored, or if the stored credentials are invalid,
the OAuth2 flow is completed to obtain the new credentials.
Returns:
Credentials, the obtained credential.
"""
home_dir = os.path.expanduser('~')
credential_dir = os.path.join(home_dir, '.credentials')
if not os.path.exists(credential_dir):
os.makedirs(credential_dir)
credential_path = os.path.join(credential_dir, 'calendar-python-quickstart.json')
store = oauth2client.file.Storage(credential_path)
credentials = store.get()
if not credentials or credentials.invalid:
flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
flow.user_agent = APPLICATION_NAME
if flags:
credentials = tools.run_flow(flow, store, flags)
else: # Needed only for compatibility with Python 2.6
credentials = tools.run(flow, store)
print('Storing credentials to ' + credential_path)
return credentials
def sheets_quickstart():
"""Shows basic usage of the Sheets API.
Creates a Sheets API service object and prints the names and majors of
students in a sample spreadsheet:
https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
"""
credentials = get_sheets_credentials()
http = credentials.authorize(httplib2.Http())
discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?'
'version=v4')
service = discovery.build('sheets', 'v4', http=http,
discoveryServiceUrl=discoveryUrl)
spreadsheetId = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
rangeName = 'Class Data!A2:E'
result = service.spreadsheets().values().get(
spreadsheetId=spreadsheetId, range=rangeName).execute()
values = result.get('values', [])
if not values:
print('No data found.')
else:
print('Name, Major:')
for row in values:
# Print columns A and E, which correspond to indices 0 and 4.
print('%s, %s' % (row[0], row[4]))
def calendar_quickstart():
"""Shows basic usage of the Google Calendar API.
Creates a Google Calendar API service object and outputs a list of the next
10 events on the user's calendar.
"""
credentials = get_calendar_credentials()
http = credentials.authorize(httplib2.Http())
service = discovery.build('calendar', 'v3', http=http)
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
print('Getting the upcoming 10 events')
eventsResult = service.events().list(
calendarId='primary', timeMin=now, maxResults=10, singleEvents=True,
orderBy='startTime').execute()
events = eventsResult.get('items', [])
if not events:
print('No upcoming events found.')
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
print(start, event['summary'])
if __name__ == '__main__':
main()

10
ScrollUtil.py Normal file
View File

@ -0,0 +1,10 @@
"""
This file contains util for scroll polling
Only really kept separate for neatness sake.
"""
"""
Tells a user their scroll, or a for_users scrolls.
Is limited to telling people
"""
def hanleScrollMsg(slack, sheet_service, msg):

61
SheetUtil.py Normal file
View File

@ -0,0 +1,61 @@
"""
This file contains general utility for polling google sheets.
Note that it is not used for the REQUISITION of the service,
as that is in the domain of GoogleApi.py.
It is instead for more general things, like parsing a sheet into 2d array,
etc.
My goal is to abstract all of the sheets bullshit out, and make reading
from a sheet as easy as working with a 2d array, as well as making it
fairly fast.
"""
"""
Gets a spreadsheet object from a sheet id
"""
def getSpreadsheet(sheet_service, sheet_id):
#Get the spreadsheet
spreadsheet = sheet_service.spreadsheets().get(spreadsheetId=sheet_id).execute()
#And let him have it!
return spreadsheet
"""
Gets the names of every page in a spreadsheet.
"""
def getPageNames(spreadsheet):
pageNames = [sheet["properties"]["title"] for sheet in spreadsheet["sheets"]]
return pageNames
"""
Gets the contents of a page in its entirety, as a 2d array.
TODO: Make this take a spreadsheet object.
Unfortunately, the spreadsheet object doc is literally 20k lines long of
poorly formatted text(seriously, what the fuck)
"""
def getPageContents(sheet_service, sheet_id, pageName, range="$A1$1:$YY"):
sheet_range = pageName + "!" + range
values = sheet_service.spreadsheets().values()
result = values.get(spreadsheetId=sheet_id, range=sheet_range).execute()
return result.get('values', [])
"""
Gets all pages as 2d arrays. from spreadsheet.
Pages are appended, in order.
So basically, you get an array of 2d arrays representing spreadsheet pages
"""
def getAllPageValues(sheet_service, sheet_id):
#Get all page names
pageNames = getPageNames(getSpreadsheet(sheet_service, sheet_id))
#Get values for each page
pageContents = [getPageContents(sheet_service, sheet_id, name, range="A2:D") for name in pageNames]
return pageContents

66
SlackUtil.py Normal file
View File

@ -0,0 +1,66 @@
from time import sleep
import re #Regular expressions
"""
Slack helpers. Separated for compartmentalization
"""
DEFAULT_USERNAME = "cylon" #NICE MEME
"""
Sends message with "text" as its content to the channel that message came from
"""
def reply(slack, msg, text, username=DEFAULT_USERNAME):
channel = msg['channel']
slack.api_call("chat.postMessage", channel=channel, text=text, username=username)
"""
Returns whether or not msg came from bot
"""
def isBotMessage(msg):
return ("bot_id" in msg or "user" not in msg)
"""
Generator that yields messages from slack.
Messages are in standard api format, look it up.
Checks on 2 second intervals (may be changed)
"""
def messageFeed(slack):
if slack.rtm_connect():
print("Waiting for messages")
while True:
sleep(2)
update = slack.rtm_read()
for item in update:
if item['type'] == 'message':
yield item
print("Critical slack connection failure")
return
"""
Returns whether or not user has configured profile
"""
def isValidProfile(user):
return ('profile' in user['user'] and
'first_name' in user['user']['profile'] and
'last_name' in user['user']['profile'])
"""
Gets the user info for whoever is first mentioned in the message,
or None if no mention is made
"""
def getForUser(slack, msg):
m_re = re.compile(".*?<@([A-Z0-9]*?)>")
mention_match = m_re.match(msg['text'])
if mention_match is not None:
mention_id = mention_match.group(1)
return slack.api_call("users.info", user=mention_id)
else:
return None

87
WaitonBot.py Executable file
View File

@ -0,0 +1,87 @@
import GoogleApi as google#For read drive
from slackclient import SlackClient#Obvious
from SlackUtil import *
from WaitonUtil import handleWaitonMsg #For waitons
#Read api token from file
apifile = open("apitoken.txt", 'r')
SLACK_API = next(apifile).strip()
apifile.close();
#Read killswitch from file
killswitchfile = open("killswitch.txt", 'r')
killswitch = next(killswitchfile).strip()
killswitchfile.close()
#Set default username.
#Authenticate, get sheets service. Done globally so we dont have to do this
#every fucking time, which is probably a bad idea
sheet_credentials = google.get_sheets_credentials()
sheet_service = google.get_sheets_service(sheet_credentials)
"""
Insults the scrub who tried to use the bot without setting up slack.
If for_user is supplied, assumes user was fine and insults for_user
Also provides helpful info for how to avoid future disgrace, but thats like, tertiary
"""
def handleProfilelessScum(slack, msg, user, for_user=None):
f_response = None
if for_user:
user_name = user['user']['name']
for_name = for_user['user']['name']
f_response = "Hey {0}, tell {1} to set up his fucking profile. Like, first and last name, and stuff".format(user_name, for_name)
else:
f_response = "Set up your profile before talking to me, scum.\n\nThat is to say, fill out your first and last name in your slack user profile! Please use what would be on the waiton list (IE your proper name, not a nickname)."
reply(slack, msg, f_response)
def main():
#Init slack
slack = SlackClient(SLACK_API)
print(slack)
slack.api_call("chat.postMessage", channel="@jacobhenry", text="I'm back baby!")
feed = messageFeed(slack)
for msg in feed:
#Check not bot
if isBotMessage(msg):
print("Message skipped. Reason: bot")
continue
#get user info
userid = msg['user']
user = slack.api_call("users.info", user=userid)
#If a mention is found, assign for_user
for_user = getForUser(slack, msg)
#Handle Message
text = msg['text'].lower()
if not isValidProfile(user):#invalid profile
handleProfilelessScum(slack, msg, user)
elif for_user and not isValidProfile(for_user):#invalid for_user profile
handleProfilelessScum(slack, msg, user, for_user)
elif "waiton" in text:
handleWaitonMsg(slack, sheet_service, msg, user, for_user)
elif "scroll" in text:
handleScrollMsg(slack, sheet_service, msg)
elif "housejob" in text:
reply(slack, msg, "I cannot do that (yet)", username="sadbot")
elif killswitch == msg['text'].lower():
reply(slack, msg, "as you wish...", username="rip bot")
break
else:
print("Message skipped. Reason: no command found")
#run main
main()

136
WaitonUtil.py Normal file
View File

@ -0,0 +1,136 @@
import SheetUtil #For read from waiton sheet
from tabulate import tabulate
from SlackUtil import reply
"""
This file contains util for waiton polling.
Only really kept separate for neatness sake.
"""
#ID of waiton sheet on drive
WAITON_SHEET_ID = "1J3WDe-OI7YjtDv6mMlM1PN3UlfZo8_y9GBVNBEPwOhE"
"""
Pulls waiton data from the spreadsheet.
Returns waitons as list of objects, each of form
{
name: <brothername>,
date: <date as string>,
meal: <meal as string>
}
"""
def getWaitons(sheet_service):
#Propogate dates to each row
def fixDates(values):
curr_date = None
curr_dow = None
last_row = None
for row in values:
date_col = row[0]
#Update date if it is more or less a date
if "/" in date_col:
curr_date = date_col
#Update previous row
if curr_date is not None:
last_row[0] = curr_dow + " - " + curr_date
#Update DOW now that previous row will not be affected "" != date_col:
if "/" not in date_col and "" != date_col:
curr_dow = date_col
#Cycle last_row
last_row = row
#Fix the last row
if last_row is not None:
last_row[0] = curr_dow + " - " + curr_date
#Propogate meal data to each row
def fixMeals(values):
curr_meal = None
for row in values:
#Update curr meal:
if row[1] != "":
curr_meal = row[1]
if curr_meal is not None:
row[1] = curr_meal
#Helper to remove steward rows
def filterStewards(values):
return [row for row in values if len(row) > 2]
#Helper to remove empty rows (IE no assignees)
def filterUnset(values):
return [row for row in values if "---" not in row[2]]
pageContents = SheetUtil.getAllPageValues(sheet_service, WAITON_SHEET_ID)
#Filter junk rows
pageContents = [filterStewards(x) for x in pageContents]
pageContents = [filterUnset(x) for x in pageContents]
#Fix stuff (make each have full info)
for pc in pageContents:
fixDates(pc)
fixMeals(pc)
#Merge, using property of python list concatenation via add (+)
allWaitons = sum(pageContents, [])
#Parse to objects
waitonObjects = [{
"name": row[2],
"date": row[0],
"meal": row[1]
} for row in allWaitons]
return waitonObjects
"""
Takes a slack context, a message to reply to, and a user to look up waitons for.
Additionally, takes a for_user flag for message formatting
IE "user" asked for "for_user" waitons, so blah blah blah
"""
def handleWaitonMsg(slack, sheet_service, msg, user, for_user=None):
"""
Filters to waitons with the given name
"""
def filterWaitons(waitons, first, last):
def valid(waiton):
return first in waiton["name"] or last in waiton["name"]
return [w for w in waitons if valid(w)]
"""
Formats waitons for output
"""
def formatWaitons(waitons):
waitonRows = [(w["name"], w["date"], w["meal"]) for w in waitons]
return tabulate(waitonRows)
#Create format string
response = ( "{0} asked for waiton information{1}, so here are his waitons:\n"
"{2}")
#Get names of caller
requestor_first = user['user']['profile']['first_name']
requestor_last = user['user']['profile']['last_name']
#Get names of target user
for_first = (for_user or user)['user']['profile']['first_name']
for_last = (for_user or user)['user']['profile']['last_name']
#Get waitons for target user
waitons = getWaitons(sheet_service)
waiton_string = formatWaitons( filterWaitons(waitons, for_first, for_last))
f_response = response.format(requestor_first, " for {0}".format(for_first) if for_user else "", waiton_string)
reply(slack, msg, f_response, username="mealbot")

13
runloop.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
#This is just a basic loop to run in the background on my raspi.
#Restarts the bot if it dies, and notifies me
while :
do
echo "Press [CTRL+C] to stop..."
sleep 5
python3 WaitonBot.py
sleep 1
python3 CallOfTheVoid.py
done