diff --git a/.gitignore b/.gitignore index 5279b2c..c3c9e81 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ venv ACADEMIC_CALENDAR_*.pdf Academic_Cal-j/** final.json +erpcreds.py # auto login erpcreds.py diff --git a/gyft.py b/gyft.py index 33e768d..d8987bf 100644 --- a/gyft.py +++ b/gyft.py @@ -27,6 +27,10 @@ def parse_args(): action="store_true", help="Delete events automatically added by the script before adding new events", ) + + parser.add_argument('-c', "--cal", dest='cal', default="primary", + help="Name of the calendar in which the events will be added to, you will have to manually create a calendar on the google calendar app/website",) + args = parser.parse_args() return args @@ -69,8 +73,10 @@ def main(): print("3. Exit") choice = int(input("Enter your choice: ")) + print(args.cal) + if choice == 1: - create_calendar(courses) + create_calendar(courses,args.cal) elif choice == 2: generate_ics(courses, output_filename) else: diff --git a/timetable/google_calendar.py b/timetable/google_calendar.py index 343991f..79395cf 100644 --- a/timetable/google_calendar.py +++ b/timetable/google_calendar.py @@ -1,23 +1,22 @@ from __future__ import print_function +from datetime import datetime import os +from bs4 import BeautifulSoup +from googleapiclient.discovery import re import httplib2 from apiclient import discovery +import icalendar from oauth2client import client, file, tools -from oauth2client import file -from oauth2client import tools -from datetime import datetime, timedelta, date -from collections import defaultdict -from icalendar import Event +from icalendar import Calendar +from timetable.generate_ics import generate_ics from utils import ( END_TERM_BEGIN, SEM_BEGIN, GYFT_RECUR_STRS, - get_rfc_time, - hdays, - holidays, ) from timetable import Course +from utils.dates import MID_TERM_END DEBUG = False @@ -25,6 +24,106 @@ CLIENT_SECRET_FILE = "client_secret.json" APPLICATION_NAME = "gyft" +def is_in_correct_sem(dt: datetime) -> bool: + if(datetime.now().replace(tzinfo=None) <= MID_TERM_END.replace(tzinfo=None)): + return dt.replace(tzinfo=None) < MID_TERM_END.replace(tzinfo=None) + elif( datetime.now().replace(tzinfo=None) >= MID_TERM_END.replace(tzinfo=None)): + return dt.replace(tzinfo=None) < END_TERM_BEGIN.replace(tzinfo=None) and dt.replace(tzinfo=None) > MID_TERM_END.replace(tzinfo=None) + else: + return True + +def parse_ics(ics,length): + events = [] + with open(ics, 'r') as rf: + ical = Calendar().from_ical(rf.read()) + for i, comp in enumerate(ical.walk()): + if ((comp.name == "VEVENT") and ( length == "c" and is_in_correct_sem(comp.get('dtstart').dt)) ) : + event = {} + for name, prop in comp.property_items(): + + if name in ["SUMMARY", "LOCATION"]: + event[name.lower()] = prop.to_ical().decode('utf-8') + + elif name == "DTSTART": + event["start"] = { + "dateTime": prop.dt.isoformat(), + "timeZone": ( str(prop.dt.tzinfo) if prop.dt.tzinfo else "Asia/Kolkata" ) + } + + elif name == "DTEND": + event["end"] = { + "dateTime": prop.dt.isoformat(), + "timeZone": ( str(prop.dt.tzinfo) if prop.dt.tzinfo else "Asia/Kolkata" ) + } + elif name == "RRULE": + freq = str(prop.get("FREQ")[0]).strip() + duration = comp.get('duration').dt + end_time = (comp.get('dtstart').dt + duration) + until = prop.get('UNTIL')[0] + event["recurrence"] = [ + ("RRULE:FREQ="+freq+";UNTIL={}").format( + until.strftime("%Y%m%dT000000Z") + ) + ] + event["end"]= { + 'dateTime': end_time.isoformat(), + "timeZone": "Asia/Kolkata" + } + elif name == "SEQUENCE": + event[name.lower()] = prop + + elif name == "TRANSP": + event["transparency"] = prop.lower() + + elif name == "CLASS": + event["visibility"] = prop.lower() + + elif name == "ORGANIZER": + event["organizer"] = { + "displayName": prop.params.get("CN") or '', + "email": re.match('mailto:(.*)', prop).group(1) or '' + } + + elif name == "DESCRIPTION": + desc = prop.to_ical().decode('utf-8') + desc = desc.replace(u'\xa0', u' ') + if name.lower() in event: + event[name.lower()] = desc + '\r\n' + event[name.lower()] + else: + event[name.lower()] = desc + + elif name == 'X-ALT-DESC' and "description" not in event: + soup = BeautifulSoup(prop, 'lxml') + desc = soup.body.text.replace(u'\xa0', u' ') + if 'description' in event: + event['description'] += '\r\n' + desc + else: + event['description'] = desc + + elif name == 'ATTENDEE': + if 'attendees' not in event: + event["attendees"] = [] + RSVP = prop.params.get('RSVP') or '' + RSVP = 'RSVP={}'.format('TRUE:{}'.format(prop) if RSVP == 'TRUE' else RSVP) + ROLE = prop.params.get('ROLE') or '' + event['attendees'].append({ + 'displayName': prop.params.get('CN') or '', + 'email': re.match('mailto:(.*)', prop).group(1) or '', + 'comment': ROLE + # 'comment': '{};{}'.format(RSVP, ROLE) + }) + + # VALARM: only remind by UI popup + elif name == 'ACTION': + event['reminders'] = {'useDefault': True} + + else: + # print(name) + pass + + events.append(event) + + return events def get_credentials() -> client.Credentials: """Gets valid user credentials from storage. @@ -53,7 +152,28 @@ def get_credentials() -> client.Credentials: return credentials -def create_calendar(courses: list[Course]) -> None: +def get_calendarId(service, summary): + page_token = None + while True: + calendar_list = service.calendarList().list(pageToken=page_token).execute() + for calendar_list_entry in calendar_list['items']: + if calendar_list_entry['summary'] == summary: + return calendar_list_entry['id'] + page_token = calendar_list.get('nextPageToken') + if not page_token: + return "primary" + + + +def cb_insert_event(request_id, response, e): + summary = response['summary'] if response and 'summary' in response else '?' + if not e: + print('({}) - Insert event {}'.format(request_id, summary)) + else: + print('({}) - Exception {}'.format(request_id, e)) + + +def create_calendar(courses: list[Course], cal_name:str) -> None: r""" Adds courses to Google Calendar Args: @@ -62,77 +182,31 @@ def create_calendar(courses: list[Course]) -> None: credentials = get_credentials() http = credentials.authorize(httplib2.Http()) service = discovery.build("calendar", "v3", http=http) - batch = service.new_batch_http_request() # To add events in a batch - for course in courses: - event = { - "summary": course.title, - "location": course.get_location(), - "start": { - "dateTime": get_rfc_time(course.start_time, course.day)[:-7], - "timeZone": "Asia/Kolkata", - }, - "end": { - "dateTime": get_rfc_time(course.end_time, course.day)[:-7], - "timeZone": "Asia/Kolkata", - }, - } - - ### making a string to pass in exdate. Changed the time of the string to class start time - exdate_str_dict = defaultdict(str) - - for day in hdays[course.day]: - exdate_str_dict[course.day] += ( - day.replace(hour=course.start_time).strftime("%Y%m%dT%H%M%S") + "," - ) - if exdate_str_dict[course.day] != None: - exdate_str_dict[course.day] = exdate_str_dict[course.day][:-1] - - if ( - exdate_str_dict[course.day] != None - ): ## if holiday exists on this recurrence, skip it with exdate - event["recurrence"] = [ - "EXDATE;TZID=Asia/Kolkata:{}".format(exdate_str_dict[course.day]), - "RRULE:FREQ=WEEKLY;UNTIL={}".format( - END_TERM_BEGIN.strftime("%Y%m%dT000000Z") - ), - ] - + batch = service.new_batch_http_request(callback=cb_insert_event) # To add events in a batch + filename = "timetable.ics" if os.path.exists("timetable.ics") else "temp.ics" + if(filename == "timetable.ics"): + print("Using existing timetable.ics file, press 'n' to generate and use a temporary one or 'y' to continue : (Y/n)") + if(input().lower() == "n" and True): + generate_ics(courses, "temp.ics") else: - event["recurrence"] = [ - "RRULE:FREQ=WEEKLY;UNTIL={}".format( - END_TERM_BEGIN.strftime("%Y%m%dT000000Z") - ) - ] - - batch.add(service.events().insert(calendarId="primary", body=event)) - print("Added " + event["summary"]) - - batch.execute() ## execute batch of timetable - - # add holidays to calender as events - for holiday in holidays: - if ( - holiday[1].date() >= date.today() - and holiday[1].date() <= END_TERM_BEGIN.date() - ): - holiday_event = { - "summary": "INSTITUTE HOLIDAY : " + holiday[0], - "start": { - "dateTime": holiday[1].strftime("%Y-%m-%dT00:00:00"), - "timeZone": "Asia/Kolkata", - }, - "end": { - "dateTime": (holiday[1] + timedelta(days=1)).strftime( - "%Y-%m-%dT00:00:00" - ), - "timeZone": "Asia/Kolkata", - }, - } - insert = ( - service.events() - .insert(calendarId="primary", body=holiday_event) - .execute() - ) + print("Invalid input") + exit(1) + + calendar_id = get_calendarId(service,cal_name) + length = input("Do you want Events from (C)urrent or (B)oth Semesters (C/b) _default is current_ : ") or 'c' + if(length.lower() == 'b'): + print("WARNING: Events from both semesters will be added.\n This may result in duplicate events if tool is used in both semesters") + events = parse_ics(filename, length.lower()) + + for i, event in enumerate(events): + try: + print("[ADDING EVENT]: ",event,"\n") + batch.add(service.events().insert(calendarId=calendar_id, body=event)) + except Exception as e: + print(e) + batch.execute(http=http) + if(os.path.exists("temp.ics")): + os.remove("temp.ics") print("\nAll events added successfully!\n")