OK, so here is some python code for any hackers that can grok it
#!/usr/bin/python
# -*- coding: utf-8 -*-
import argparse
import inspect
import sys
import os
import requests
import json
import datetime
from subprocess import Popen, PIPE
args = None
###
### begin - this is the start of the program. begin is actually called at the end of this file.
###
### parse the command line then create events
###
def begin():
parser = argparse.ArgumentParser()
parser.add_argument("-v","--verbose", help="increase verbosity. use more than once to increase verbosity", action="count")
parser.add_argument("-t","--trooptrack", help="connect to, and then add the calendar to TroopTrack system using the login username/password", metavar='<username/password>')
global args
args = parser.parse_args()
if args.verbose : print("We be up!")
else: args.verbose = 0
tt = trooptrack()
events = [{'description': 'Regular Troop Meetings on 1st & 3rd Tuesdays of each month.\nArrive promptly for sign offs with Scoutmaster or Assistant Scoutmaster, meeting starts at 15 after.\n\n<b>Wear class A uniform</b>\n', 'start': datetime.datetime(2014, 1, 2, 19, 0), 'end': datetime.datetime(2014, 1, 2, 20, 30), 'who': ['everyone'], 'where': 'Church', 'invite': '1', 'title': 'Troop Meeting', 'type': 'Meeting'}]
for x in events:
tt.newEvent(x)
if args.verbose : print("### DONE ####")
###
### AttrDisplay - base class that pretty prints its class
###
class AttrDisplay:
def gatherAttrs(self):
attrs = []
for key in sorted(self.__dict__):
attrs.append('{}: {}'.format(key, getattr(self,key)))
return "\n".join(attrs)
def __repr__(self):
return '{} = [\n{}\n]\n\n'.format(self.__class__.__name__, self.gatherAttrs())
def doAppleScript(scpt):
if type(scpt) == type('') :
scpt = str.encode(scpt)
rslts = ex(['/usr/bin/osascript', '-'], stdin=scpt)
if rslts['rc'] != 0 :
print( '### Command:\n\t{}\n\tfails with:\n\t{}'.format(rslts['command'], rslts['stderr']) )
sys.exit(rslts['rc'])
return rslts
def ex(args, stdin=None):
io = {}
io['command'] = ' '.join(map(str,args))
if stdin != None:
io['command'] = io['command'] + " << " + stdin.decode("utf-8")
proc = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, close_fds=True)
io['stdout'], io['stderr'] = proc.communicate(stdin)
io['stdout'] = io['stdout'].decode("utf-8").rstrip()
io['stderr'] = io['stderr'].decode("utf-8").rstrip()
io['rc'] = proc.returncode
return io
proc = Popen(args, stdout=PIPE, stderr=PIPE, close_fds=True)
proc.wait()
io['stdout'] = proc.stdout.read().decode("utf-8").rstrip()
io['stderr'] = proc.stderr.read().decode("utf-8").rstrip()
io['rc'] = proc.returncode
return io
def weekdayToString(w):
junk = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday".split("|")
return junk[w]
def lastWeekday(date, weekday):
daycount = monthrange(date.year, date.month)[1]
date = date.replace(day=daycount)
while weekdayToString(date.weekday()) != weekday:
daycount -= 1
date = date.replace(day=daycount)
return date
def nthWeekday(date, n, weekday):
if n < 0: return lastWeekday(date, weekday)
date = date.replace(day=1)
while weekdayToString(date.weekday()) != weekday:
#print("WDTS:{},{},{} {},{}".format(date,n,weekday,date.weekday(),weekdayToString(date.weekday())))
date = date + datetime.timedelta(days=1)
n = n - 1
while n:
date = date + datetime.timedelta(days=7)
n = n - 1
return date
def printDict(t,x):
if t != None : print(t,end="")
pprint.pprint(x)
return
def __LINE__():
callerframerecord = inspect.stack()[2] # 0 represents this line
# 1 represents line at caller
frame = callerframerecord[0]
info = inspect.getframeinfo(frame)
return info.lineno # __LINE__ -> 13
def errorExit(s):
print("\n### ERROR ### (line {})\n{}\n".format(__LINE__(),s))
sys.exit(1)
def warning(s):
print("\n### WARNING ### (line {})\n{}\n".format(__LINE__(),s))
def getDSTOffset(d):
## for 2017 => Sun, Mar 12, 2:00 AM PST → PDT +1 hour (DST start) UTC-7h (2nd sunday)
## Sun, Nov 5, 2:00 AM PDT → PST -1 hour (DST end) UTC-8h (1st sunday)
startPDT = datetime.datetime(d.year,3,nthWeekday(datetime.date(d.year,3,1), 2, "Sunday").day,2) ## 3/12/2017 2:00AM
endPDT = datetime.datetime(d.year,11,nthWeekday(datetime.date(d.year,11,1), 1, "Sunday").day,1) ##11/5/2017 2:00AM
if args.verbose > 3 :
print("startPDT: {}".format(startPDT))
print("endPDT: {}".format(endPDT))
print("###### PDT {} #####".format(d) )
if ( d > startPDT and d < endPDT ):
return -1
return 0
class trooptrack(AttrDisplay):
# setup program globals
url = 'https://YOUR_TROOP_HERE.trooptrack.com:443/api/v1/'
headers = {'X-Partner-Token': 'PARTNER_TOKEN_HERE'}
def __init__(self):
if self.headers['X-Partner-Token'] == 'PARTNER_TOKEN_HERE':
errorExit("You have to edit the code to add your troop number and partner token.")
parts = args.trooptrack.split("/")
if len(parts) > 2 :
errorExit("--trooptrack <username/password> had more than one /")
if args.verbose :
print("### Connecting to TroopTrack...")
r = self.ttRequest('tokens', data={}, headers={'X-Username':parts[0],'X-User-Password':parts[1]})
rsp = r.json()['users'][0]
token = rsp['token']
self.troopID = rsp['troop_id']
self.userID = rsp['user_id']
if args.verbose > 1 :
print ("Token {}".format(token) )
print ("TroopID {}".format(self.troopID) )
print ("UserID {}".format(self.userID) )
self.headers['X-User-Token'] = token
self.headers['Connection'] = 'keep-alive'
if args.verbose :
print("### Connected")
## get the event types
r = self.ttRequest('events/types').json()
# extract users
self.users = r['users'] ## 'user_id': 3778, 'name': 'first last', 'scout': True
# calendar "event" types
self.event_types = {}
for item in r['event_types'] : ## 'color': 'white', 'name': 'Campout', 'event_type_id': 66837
self.event_types[item['name']] = str(item['event_type_id'])
# array of calendar event invitees
self.patrols = {}
for item in r['patrols']:
self.patrols[item['name'].lower()] = 'Patrol-{}'.format(item['patrol_id'])
self.patrols['everyone'] = 'Troop-{}'.format(self.troopID) ## special 'patrol' for everyone
if args.verbose > 1:
printDict("#PATROLS:",self.patrols)
# array of mailing lists (custom mailing list names)
self.mailingLists = {}
r = self.ttRequest('mailing_lists').json()['mailing_lists']
for l in r:
self.mailingLists[l['name'].lower()] = {'id':l['mailing_list_id'],'email':l['email']}
if args.verbose > 1:
print("### {} event types".format(len(self.event_types)))
print("### {} patrols".format(len(self.patrols)))
print("### {} users".format(len(self.users)))
def ttDate(self,d):
return "{:4d}-{:02d}-{:02d}".format(d.year,d.month,d.day)
def ttDatetime(self,d):
gmt = 8 + getDSTOffset(d)
#print("GMT offst is {} for {}".format(gmt,d))
rslt = "{:4d}-{:02d}-{:02d}T{:02d}:{:02d}-0{}00".format(d.year,d.month,d.day,d.hour,d.minute,gmt)
#print("TT DATE {}".format(rslt))
return rslt
def ttRequest(self,req,data=None,headers=None):
# all the heavy lifting happens in here
requestURL = self.url + req
requestHeaders = dict(self.headers)
if headers != None :
for k,v in headers.items():
requestHeaders[k] = v
if args.verbose > 2 :
print("####### ####### ####### ####### ####### ####### #######\nttRequest url: {}".format(requestURL))
printDict("## headers:", requestHeaders)
if data != None :
printDict("## data:", data)
print("#######\n")
if 'Content-Type' in requestHeaders and requestHeaders['Content-Type'] == "application/json":
data = json.dumps(data)
if data != None :
rslt = requests.post(requestURL, headers=requestHeaders, data=data) # probably need to be surrounded by a "try"
else:
rslt = requests.get(requestURL, headers=requestHeaders) # probably need to be surrounded by a "try"
if rslt.status_code < 200 or rslt.status_code > 201 :
printDict("#request headers:", rslt.request.headers)
print("#request body: ", rslt.request.body)
print("#result reason: ", rslt.reason)
printDict("#result headers: ", rslt.headers)
print("#result text: ", rslt.text)
errorExit('TroopTrack request failed with HTTP result code ({}) '.format(rslt.status_code))
if args.verbose > 1 :
j = rslt.json()
## keep the noise down
if 'event' in j:
j = j['event']
if 'event_types' in j and 'users' in j:
j['users'] = None
print( json.dumps(j, sort_keys=True, indent=2) + "\n----------------" )
return rslt
def newEvent(self,event):
if args.verbose :
print("### creating TroopTrack event {} - {}".format(self.ttDate(event['start']),event['title']) )
# make sure we can map the event type (data) to what TroopTrack will allow as event types
if event['type'] not in self.event_types:
errorExit("Event type '{}' not in TroopTrack event types : {}\n".format(event['type'], list(self.event_types.keys())) )
# reformat \n in description into <p> for htmlism
description = ""
for p in event['description'].split("\n"):
if len(p): # only non-blank lines
if len(description):
description += "<p>" # use <p> for newlines
description += p
# build the TroopTrack request structure
eventInfo = {
'title': event['title'],
'event_type_id': self.event_types[event['type']],
'start_at': self.ttDatetime(event['start']),
'end_at': self.ttDatetime(event['end']),
'location': event['where'],
'description': description
}
## prepare mark the things that's we'll need to fixup, since the API doesn't do right or give the option
fixupFullday = fixupSelf = removeRSVP = False
# full day and no TT API for it
if 'fullday' in event:
fixupFullday = True
if event['type'] == 'Campout':
campingNights = event['end']-event['start']
eventInfo['camping_nights'] = campingNights.days
if 'rsvpBy' in event:
eventInfo['rsvp_deadline'] = self.ttDate(event['rsvpBy'])
else:
# can't turn off RSVP with API
removeRSVP = True
if 'invite' in event:
eventInfo['send_invites_when'] = event['invite']
if 'reminder' in event:
eventInfo['send_reminder_when'] = event['reminder']
eventInfo['inviteable_tokens'] = []
if 'who' in event and len(event['who']) :
for person in event['who'] :
person = person.lower()
if person in self.patrols:
eventInfo['inviteable_tokens'].append('{}'.format(self.patrols[person]))
else:
if person in self.mailingLists:
eventInfo['inviteable_tokens'].append('MailingList-{}'.format(self.mailingLists[person]['id']))
else:
eventInfo['inviteable_tokens'] = ['User-{}'.format(self.userID)] ## can't not specify one
warning("unknown person {}. You'll need to fixup the event yourself".format(person, event))
elif 'rsvp_deadline' in eventInfo or 'send_invites_when' in eventInfo or 'send_reminder_when' in eventInfo:
## can't not specify a 'who', so have to specify self in the TT API, then remove self later
eventInfo['inviteable_tokens'].append('User-{}'.format(self.userID))
fixupSelf = True
eventInfo['guests_allowed']='no'
eventInfo['payment_required_to_rsvp']='no'
if args.verbose > 1:
printDict('## EVENT INFO: ', eventInfo)
## make the request to create the event in the calendar
r = self.ttRequest('events', data={'event':eventInfo}, headers={"Content-Type": "application/json"} )
## arrrgh, the TT API isn't quite good enough yet, go do fixups
if fixupSelf or removeRSVP or fixupFullday:
self.fixupEvent(r.json()['event']['event_id'], event, fixupSelf, removeRSVP, fixupFullday)
def fixupEvent(self, eventID, event, fixupSelf, removeRSVP, fixupFullday):
if args.verbose > 2 :
print("#################################################################################")
print("Fixups for event: {}({}) fixupSelf:{}, removeRSVP:{}, fixupFullday:{}".format(event['start'], event['title'], fixupSelf, removeRSVP, fixupFullday))
tell = '''
tell application "Safari"
if not (exists document 1) then reopen
delay 0.2
tell document 1
set URL to "https://troop466.trooptrack.com/plan/events/{0}/edit"
delay 3
-- remove RSVP checkbox if needed
if {1} then
do JavaScript "document.getElementById('event_rsvp_required').checked=false"
end if
-- remove the user checked (since it may have been added to complete the API event creation)
if {2} then
do JavaScript "document.getElementById('event_inviteable_tokens_user-{3}').checked=false"
end if
-- add the AllDay checkbox
if {4} then
do JavaScript "document.getElementById('event_all_day').checked=true"
end if
-- submit the changes
do JavaScript "document.getElementById('edit_event_{0}').submit()"
delay 3
end tell
end tell
'''.format(eventID, removeRSVP, fixupSelf, self.userID, fixupFullday )
if args.verbose > 2 :
print(tell)
doAppleScript(tell)
##########################################################################################
begin()