diff --git a/.firsttimeruncheck b/.firsttimeruncheck new file mode 100644 index 0000000..f32a580 --- /dev/null +++ b/.firsttimeruncheck @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1f95eb2..e34eb3b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # App Specific sudoku*.png data.json +client_secret.json # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/application/TODO b/application/TODO new file mode 100644 index 0000000..8d6ea0c --- /dev/null +++ b/application/TODO @@ -0,0 +1,5 @@ +TODO: +- check if first run (api call) and show google calendar login button +* schedule pypi every().day() or smth like that at (time to print)-5 minutes to update calendar and get ready to print new daily list +* parse calendar list to database format and call save_todos() with list of Todos +- Start uvicorn on device startup (using gunicorn + sysmtectl) \ No newline at end of file diff --git a/application/main.py b/application/main.py index cc3adbf..9d0b800 100644 --- a/application/main.py +++ b/application/main.py @@ -10,14 +10,17 @@ from fastapi.staticfiles import StaticFiles from .utils import VerifyToken from .database import TodoDatabase from .thermal_print import ThermalPrinter +from .sync_calendar import SyncData import errno +import os # from wsgiref import simple_server # Creates app instance app = FastAPI() auth = VerifyToken() data = TodoDatabase() +calendar = SyncData() printer = ThermalPrinter(data) # printer.print_default() #temp debug # printer.print_custom("Configuration Site: \n\n") @@ -69,21 +72,104 @@ class GoogleUpdate(BaseModel): # } # return result +# import google.oauth2.credentials +# import google_auth_oauthlib.flow +# import googleapiclient.discovery + +# from google.auth.transport.requests import Request +# from google.oauth2.credentials import Credentials +# import os +# import json + +# from google_auth_oauthlib.flow import InstalledAppFlow +# from googleapiclient.discovery import build +# from googleapiclient.errors import HttpError + +# scopes = +# SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] + @app.post("/api/google/update") -def write_todos(code: GoogleUpdate, auth_result: str = Security(auth.verify)): - print(code.code) - return {"status": "success"} +def google_update(googleCode: GoogleUpdate, auth_result: str = Security(auth.verify)): + + if calendar.get_new_creds(googleCode.code): + calendar.get_calendar_events() + with open(".firsttimeruncheck", 'w') as f: + f.write("true") + return {"status": "success"} + return {"status": "error"} + + + # print(googleCode.code) + + # creds = None + + # if os.path.exists('token.json'): + # creds = Credentials.from_authorized_user_file('token.json', SCOPES) + # # else: + # # flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + # # 'client_secret.json', + # # scopes=['https://www.googleapis.com/auth/calendar.readonly'], + # # redirect_uri="postmessage" + # # # state=state + # # ) + + # # creds = flow.fetch_token(code=googleCode.code) + # # print(creds) + # # with open("token.json", 'w') as token: + # # token.write(creds.to_json()) + + # if not creds or not creds.valid: + # if creds and creds.expired and creds.refresh_token: + # creds.refresh(Request()) + # else: + # # flow = InstalledAppFlow.from_client_secrets_file( + # # 'credentials.json', SCOPES) + # # creds = flow.run_local_server(port=0) + + # flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + # 'client_secret.json', + # scopes=SCOPES, + # redirect_uri="postmessage" + # # state=state + # ) + # flow.fetch_token(code=googleCode.code) + # creds = flow.credentials + # # print(creds) + + # # Save the credentials for the next run + # with open('token.json', 'w') as token: + # token.write(creds.to_json()) + # # json.dump(token, creds) + + + # # if not creds or not creds.valid: + # # return {"status": "google already autherized"} + # # creds = Credentials.from_authorized_user_file('token.json', SCOPES) + + + + @app.get('/api/first_run') def first_run(): - try: - with open('.firsttimeruncheck', 'x') as f: - f.write("") - return {"status": "First Run"} - except OSError as e: - print(e) - if e.errno == errno.EEXIST: - return {"status": "Not First Run"} + if os.path.exists('.firsttimeruncheck'): + with open('.firsttimeruncheck', 'r') as f: + data = f.read() + if data == "true": + return {"status": "Not First Run"} + else: + with open('.firsttimeruncheck', 'w') as f: + f.write("false") + + return {"status": "First Run"} + # try: + # with open('.firsttimeruncheck', 'x') as f: + # f.write("") + # return {"status": "First Run"} + # except OSError as e: + # print(e) + # if e.errno == errno.EEXIST: + # return {"status": "Not First Run"} diff --git a/application/static/index.html b/application/static/index.html index 5cf8970..579f418 100644 --- a/application/static/index.html +++ b/application/static/index.html @@ -18,8 +18,17 @@ - + @@ -453,28 +462,83 @@ diff --git a/application/static/scripts.js b/application/static/scripts.js index 82223dd..e7aa801 100644 --- a/application/static/scripts.js +++ b/application/static/scripts.js @@ -638,6 +638,7 @@ function loginAction(e) { // console.log(t) jToken = t getTodosFromAPI() + mainAsync() // document.getElementById("testToken").innerHTML = t @@ -670,6 +671,79 @@ function loginAction(e) { }; +let client = null + +async function mainAsync() { + console.log('run async') + async function check_first_run() { + let res = await fetch("/api/first_run") + let resJ = await res.json() + return resJ + // return fetch("/api/first_run").then(r => { + // return r.json().then(j => { + // first_run = j + // console.log(j) + // return j + // }) + // }) + } + + let first_run_status = await check_first_run() + if (first_run_status["status"] === "First Run") { + document.getElementById("googleAuthModel").classList.remove("hidden") + let code_receiver_uri = "/api/google/update" + + client = google.accounts.oauth2.initCodeClient({ + client_id: '186960779149-ejtu6hh3kdatlouau80h2pivt4tv3hd0.apps.googleusercontent.com', + scope: 'https://www.googleapis.com/auth/calendar.readonly', + ux_mode: 'popup', + callback: (response) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', code_receiver_uri, true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Authorization', 'Bearer ' + jToken); + // Set custom header for CRSF + xhr.setRequestHeader('X-Requested-With', 'XmlHttpRequest'); + xhr.onload = function() { + console.log('Auth code response: ' + xhr.responseText); + document.getElementById("googleAuthModel").classList.add("hidden") + }; + xhr.send(JSON.stringify({"code": response.code})); + console.log(xhr) + console.log(response.code) + }, + }); + + + + } + + // console.log(test) + + // console.log(first_run) + + + // function check_first_run() { + // let first_run = "" + // const req = new XMLHttpRequest(); + // // req.addEventListener("load", reqListener); + // req.open("GET", "/api/first_run", false); + // req.onload = function() { + // first_run = req.responseText + // // console.log(first_run) + // } + // req.send(); + // return first_run + // console.log(first_run) + // } + + // console.log(check_first_run()) + + + +} + + function getTodosFromAPI(day=dayjs().format("YYYY-MM-DD")) { document.getElementById("loginPanel").classList.add("hidden") diff --git a/application/sync_calendar.py b/application/sync_calendar.py index a7294ed..5c82d05 100644 --- a/application/sync_calendar.py +++ b/application/sync_calendar.py @@ -1,7 +1,164 @@ #TODO: All Google/iCloud Calendar syncing logic here +import google_auth_oauthlib.flow +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +import os +import json +import datetime + + + +from .database import TodoDatabase + +data = TodoDatabase() + + +from datetime import datetime, time, timedelta +import pytz +from zoneinfo import ZoneInfo +from datetime import timezone + +def midnight_UTC(offset): + + # Construct a timezone object + tz = pytz.timezone('America/Toronto') + + # Work out today/now as a timezone-aware datetime + today = datetime.now(tz) + + # Adjust by the offset. Note that that adding 1 day might actually move us 23 or 25 + # hours into the future, depending on daylight savings. This works because the {today} + # variable is timezone aware + target_day = today + timedelta(days=1) * offset + + # Discard hours, minutes, seconds and microseconds + midnight_aware = tz.localize( + datetime.combine(target_day, time(0, 0, 0, 0)), is_dst=None) + + # Convert to UTC + midnight_UTC = midnight_aware.astimezone(pytz.utc) + + return midnight_UTC + + + class SyncData(): def __init__(self): - pass + self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] + self.creds = None + + self.get_new_creds() + self.get_calendar_events() + + + def get_new_creds(self, code=""): + """Returns True if successfully obtains credentials, + otherwise throws errors according to functions called, unless + code='' in which case if no saved credentials are found, returns False + """ + if os.path.exists('token.json'): + self.creds = Credentials.from_authorized_user_file('token.json', self.SCOPES) + # else: + # flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + # 'client_secret.json', + # scopes=['https://www.googleapis.com/auth/calendar.readonly'], + # redirect_uri="postmessage" + # # state=state + # ) + + # creds = flow.fetch_token(code=googleCode.code) + # print(creds) + # with open("token.json", 'w') as token: + # token.write(creds.to_json()) + + if not self.creds or not self.creds.valid: + if self.creds and self.creds.expired and self.creds.refresh_token: + self.creds.refresh(Request()) + else: + if code == "": + return False + # flow = InstalledAppFlow.from_client_secrets_file( + # 'credentials.json', SCOPES) + # creds = flow.run_local_server(port=0) + + flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + 'client_secret.json', + scopes=self.SCOPES, + redirect_uri="postmessage" + # state=state + ) + flow.fetch_token(code=code) + self.creds = flow.credentials + # print(creds) + + # Save the credentials for the next run + with open('token.json', 'w') as token: + token.write(self.creds.to_json()) + # json.dump(token, creds) + return True + + + def get_calendar_events(self, day=None): + self.get_new_creds() + + # now = datetime.now() + # if day: + # now = datetime.datetime.strptime(day, "%Y-%m-%d") + # else: + # now = datetime.datetime.now() + # day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + # day_end = day_start + datetime.timedelta(hours=24) + # day_start = day_start.isoformat() + "Z" + # day_end = day_end.isoformat() + "Z" + + if day: + pass + else: + # day_start = midnight_UTC(0).isoformat() + "Z" + # day_end = midnight_UTC(1).isoformat() + "Z" + day_start = datetime.combine(datetime.now(tz=ZoneInfo("America/Toronto")).date(), time(0, 0), tzinfo=ZoneInfo("America/Toronto")) + day_end = day_start + timedelta(hours=23,minutes=59) + # day_start_for = day_start.astimezone(ZoneInfo("UTC")).isoformat() + "Z" + # day_end_for = day_end.astimezone(ZoneInfo("UTC")).isoformat() + "Z" + day_start_for = day_start.astimezone(ZoneInfo("UTC")).strftime('%Y-%m-%dT%H:%M:%S.%fZ') + day_end_for = day_end.astimezone(ZoneInfo("UTC")).strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + # print(day_start.tzname()) + print(day_start) + print(day_end) + print(day_start.astimezone(ZoneInfo("UTC")).strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + print(day_end.astimezone(ZoneInfo("UTC")).strftime('%Y-%m-%dT%H:%M:%S.%fZ')) + print(datetime.utcnow().isoformat() + 'Z') + + + try: + service = build('calendar', 'v3', credentials=self.creds) + + # Call the Calendar API + # now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time + print('Getting all upcoming events for time range: ' + day_start_for + " to " + day_end_for) + events_result = service.events().list(calendarId='primary', timeMin=day_start_for, timeMax=day_end_for, + singleEvents=True, orderBy='startTime').execute() + events = events_result.get('items', []) + + if not events: + print('No upcoming events found.') + return + + # Prints the start and name of the next 10 events + # event format: {'kind': 'calendar#event', 'etag': '"3386046190326000"', 'id': '0migcv6t729ph1kirhiauej3eh', 'status': 'confirmed', 'htmlLink': 'https://www.google.com/calendar/event?eid=MG1pZ2N2NnQ3MjlwaDFraXJoaWF1ZWozZWggYmFtLmltLnNhbUBt', 'created': '2023-08-26T04:11:35.000Z', 'updated': '2023-08-26T04:11:35.163Z', 'summary': 'test', 'creator': {'email': 'bam.im.sam@gmail.com', 'self': True}, 'organizer': {'email': 'bam.im.sam@gmail.com', 'self': True}, 'start': {'dateTime': '2023-08-27T14:30:00-04:00', 'timeZone': 'America/Toronto'}, 'end': {'dateTime': '2023-08-27T15:30:00-04:00', 'timeZone': 'America/Toronto'}, 'iCalUID': '0migcv6t729ph1kirhiauej3eh@google.com', 'sequence': 0, 'reminders': {'useDefault': True}, 'eventType': 'default'} + for event in events: + print(event) + start = event['start'].get('dateTime', event['start'].get('date')) + end = event['end'].get('dateTime', event['end'].get('date')) + print(start, "->", end, event['summary']) + + except HttpError as error: + print('An error occurred: %s' % error) + + #TODO: Obtain calendar data and save to local database, parse into printable content diff --git a/client_secret.json.example b/client_secret.json.example new file mode 100644 index 0000000..2983cd0 --- /dev/null +++ b/client_secret.json.example @@ -0,0 +1 @@ +{"web":{"client_id":"","project_id":"thermaltodos","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"","javascript_origins":["https://thermaltodos.imsam.ca"]}} \ No newline at end of file