bergercookie / syncall

Bi-directional synchronization between services such as Taskwarrior, Google Calendar, Notion, Asana, and more
MIT License
449 stars 41 forks source link

[BUG] Subprojects are not synced #83

Closed hakan-demirli closed 10 months ago

hakan-demirli commented 1 year ago

Describe the bug

Subprojects are ignored. Only willsync is synchronized and testx are ignored.

To Reproduce

task add willsync project:mainpro due:tuesday task add test1 project:mainpro.p0 due:tuesday task add test2 project:mainpro.p1 due:monday task add test3 project:mainpro.p2 due:tomorrow tw_gcal_sync -c "deadlines_tw" -p "mainpro" --verbose

Expected Behavior

It should also sync subprojects.

hakan-demirli commented 1 year ago

I made a substitute.

#! /usr/bin/env python3

import datetime
import pytz
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import subprocess
import json
from pathlib import Path

TIME_MIN = (datetime.datetime.utcnow() - datetime.timedelta(days=365)).isoformat() + 'Z'
TIME_MAX = (datetime.datetime.utcnow() + datetime.timedelta(days=365)).isoformat() + 'Z'
SCOPES = ['https://www.googleapis.com/auth/calendar']
CREDS_FILE = './cred_tw.json'
SYNCED_PROS = {'school': 'somenumber@group.calendar.google.com'}

class GoogleCalendarAPI:
    def __init__(self):
        creds = None
        root_path = Path(__file__).parent
        token_path = root_path / 'token.pickle'
        if os.path.exists(token_path):
            with open(token_path, 'rb') as token:
                creds = pickle.load(token)
        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(CREDS_FILE, SCOPES)
                creds = flow.run_local_server(port=0)
            with open(token_path, 'wb') as token:
                pickle.dump(creds, token)

        self.service = build('calendar', 'v3', credentials=creds)

    def get_events(self,calendar_id):
        events = self.service.events().list(calendarId=calendar_id, timeMin=TIME_MIN,
                                            timeMax=TIME_MAX, singleEvents=True,
                                            orderBy='startTime').execute()
        return events

    def update_event(self,calendar_id,event):
        self.service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute()

    def insert_event(self,calendar_id,event):
        self.service.events().insert(calendarId=calendar_id, body=event).execute()

    def delete_event(self,calendar_id,event):
        self.service.events().delete(calendarId=calendar_id, eventId=event['id']).execute()

def convert_task_date_str_to_obj(task):
    date_format = '%Y%m%dT%H%M%SZ'
    due_date = datetime.datetime.strptime(task["due"], date_format).replace(tzinfo=pytz.utc).astimezone()
    modi_date = datetime.datetime.strptime(task["modified"], date_format).replace(tzinfo=pytz.utc).astimezone()
    task["due"] = due_date
    task["modified"] = modi_date
    return task

def convert_tasks_date_str_to_obj(tasks):
    for task in tasks:
        task = convert_task_date_str_to_obj(task)
    return tasks

if __name__ == "__main__":
    gapi = GoogleCalendarAPI()

    for pro in SYNCED_PROS:
        cal_id = SYNCED_PROS[pro]
        events = gapi.get_events(cal_id)
        for event in events['items']:
            taskuuid = event['extendedProperties']['private']['uuid']
            task = json.loads(subprocess.run(f'task {taskuuid} export', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout)
            if not task:
                gapi.delete_event(cal_id, event)
                continue
            task = convert_task_date_str_to_obj(task[0])
            task_last_modified  = task['modified']
            cal_date = event.get('updated').replace(":","").replace("-","").split(".")[0] + 'Z'
            event_last_modified = datetime.datetime.strptime(cal_date, '%Y%m%dT%H%M%SZ').replace(tzinfo=pytz.utc).astimezone()
            if(task_last_modified > event_last_modified): # if the calendar modified date is older delete it.
                gapi.delete_event(cal_id, event)
            else:
                subprocess.run(f'task {taskuuid} modify description:{event["summary"]} due:{event["start"]["dateTime"]}', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        tasks = json.loads(subprocess.run(f'task project:{pro} export', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout)
        tasks = convert_tasks_date_str_to_obj(tasks)
        events = gapi.get_events(cal_id)
        for task in tasks:
            exists = False
            for event in events['items']:
                if(task['uuid'] == event['extendedProperties']['private']['uuid']):
                    exists = True
                    break
            if(not exists):
                start_time = (task['due'] - datetime.timedelta(hours=0, minutes=5)).isoformat()
                end_time   = (task['due'] - datetime.timedelta(hours=0, minutes=5)).isoformat()
                description = task['description']
                if(task['status'] == 'completed'):
                    description = '✅' + task['description']
                new_event = {
                    'summary': description,
                    'start': {
                        'dateTime': start_time,
                    },
                    'end': {
                        'dateTime': end_time,
                    },
                    'extendedProperties': {
                        'private': {
                            'uuid':    task['uuid'],
                            'project': task['project']
                        }
                    }
                }
                gapi.insert_event(cal_id,new_event)