ticktick_rest_api.pyā¢26 kB
#!/usr/bin/env python3
"""
TickTick REST API Client - Pure OAuth Implementation
Direct REST API integration with TickTick using official Open API.
No hacky libraries, just clean HTTP requests with OAuth 2.0.
"""
import os
import json
import webbrowser
from datetime import datetime, timedelta
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse, urlencode
import requests
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
class OAuthCallbackHandler(BaseHTTPRequestHandler):
"""HTTP server handler to receive OAuth callback"""
def do_GET(self):
"""Handle OAuth callback redirect"""
query = urlparse(self.path).query
params = parse_qs(query)
if 'code' in params:
self.server.auth_code = params['code'][0]
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b"""
<html>
<body>
<h1>Authorization Successful!</h1>
<p>You can close this window and return to the terminal.</p>
</body>
</html>
""")
else:
self.send_response(400)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b"<html><body><h1>Authorization Failed</h1></body></html>")
def log_message(self, format, *args):
"""Suppress server logs"""
pass
class TickTickClient:
"""TickTick REST API Client using OAuth 2.0"""
BASE_URL = "https://api.ticktick.com/open/v1"
AUTH_URL = "https://ticktick.com/oauth/authorize"
TOKEN_URL = "https://ticktick.com/oauth/token"
def __init__(self, client_id, client_secret, redirect_uri="http://127.0.0.1:8080"):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
self.access_token = None
self.refresh_token = None
self.token_file = ".ticktick-token.json"
# Try to load existing token
self._load_token()
def _load_token(self):
"""Load existing OAuth token from file"""
if os.path.exists(self.token_file):
try:
with open(self.token_file, 'r') as f:
data = json.load(f)
self.access_token = data.get('access_token')
self.refresh_token = data.get('refresh_token')
print(f"ā Loaded existing OAuth token from {self.token_file}")
return True
except Exception as e:
print(f"Warning: Could not load token file: {e}")
return False
def _save_token(self, token_data):
"""Save OAuth token to file"""
with open(self.token_file, 'w') as f:
json.dump(token_data, f, indent=2)
print(f"ā Saved OAuth token to {self.token_file}")
def authorize(self):
"""Perform OAuth 2.0 authorization flow"""
print("\n" + "=" * 60)
print("OAuth 2.0 Authorization")
print("=" * 60)
# Build authorization URL
auth_params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'response_type': 'code',
'scope': 'tasks:read tasks:write',
'state': 'health_marriage_tracker'
}
auth_url = f"{self.AUTH_URL}?{urlencode(auth_params)}"
print("\n1. Opening browser for authorization...")
print(f" URL: {auth_url}\n")
webbrowser.open(auth_url)
print("2. Starting local callback server on http://127.0.0.1:8080")
print(" Waiting for authorization...\n")
# Start local HTTP server to receive callback
server = HTTPServer(('127.0.0.1', 8080), OAuthCallbackHandler)
server.auth_code = None
server.timeout = 120 # 2 minute timeout
# Wait for callback
server.handle_request()
if not server.auth_code:
raise Exception("Authorization failed - no code received")
print("ā Authorization code received!\n")
# Exchange code for token
print("3. Exchanging authorization code for access token...")
token_data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'code': server.auth_code,
'grant_type': 'authorization_code',
'redirect_uri': self.redirect_uri
}
response = requests.post(
self.TOKEN_URL,
data=token_data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
if response.status_code != 200:
raise Exception(f"Token exchange failed: {response.status_code} - {response.text}")
token_response = response.json()
self.access_token = token_response['access_token']
self.refresh_token = token_response.get('refresh_token')
# Save token
self._save_token(token_response)
print("ā Access token obtained and saved!\n")
print("=" * 60)
def _request(self, method, endpoint, **kwargs):
"""Make authenticated API request"""
if not self.access_token:
raise Exception("Not authenticated - call authorize() first")
url = f"{self.BASE_URL}/{endpoint}"
headers = kwargs.pop('headers', {})
headers['Authorization'] = f"Bearer {self.access_token}"
response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code == 401:
# Token expired - try to refresh
if self.refresh_token:
print("Access token expired, refreshing...")
self._refresh_token()
# Retry request with new token
headers['Authorization'] = f"Bearer {self.access_token}"
response = requests.request(method, url, headers=headers, **kwargs)
else:
raise Exception("Access token expired and no refresh token available")
return response
def _refresh_token(self):
"""Refresh the access token"""
token_data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'refresh_token': self.refresh_token,
'grant_type': 'refresh_token'
}
response = requests.post(
self.TOKEN_URL,
data=token_data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
if response.status_code != 200:
raise Exception(f"Token refresh failed: {response.status_code} - {response.text}")
token_response = response.json()
self.access_token = token_response['access_token']
self.refresh_token = token_response.get('refresh_token', self.refresh_token)
self._save_token(token_response)
print("ā Access token refreshed")
# Project methods
def get_projects(self):
"""Get all projects"""
response = self._request('GET', 'project')
response.raise_for_status()
return response.json()
def create_project(self, name, color=None):
"""Create a new project"""
data = {'name': name}
if color:
data['color'] = color
response = self._request('POST', 'project', json=data)
response.raise_for_status()
return response.json()
# Task methods
def get_tasks(self):
"""Get all tasks"""
response = self._request('GET', 'task')
response.raise_for_status()
return response.json()
def create_task(self, title, **kwargs):
"""
Create a new task
Args:
title (str): Task title
**kwargs: Optional fields:
- projectId (str): Project ID
- content (str): Task description
- startDate (str): ISO 8601 format
- dueDate (str): ISO 8601 format
- timeZone (str): IANA timezone
- priority (int): 0, 1, 3, or 5
- isAllDay (bool): All-day task
- repeat (str): RRULE format
- reminders (list): List of TRIGGER strings
- items (list): Checklist items
"""
data = {'title': title}
data.update(kwargs)
response = self._request('POST', 'task', json=data)
response.raise_for_status()
return response.json()
def complete_task(self, task_id):
"""Mark task as complete"""
response = self._request('POST', f'task/{task_id}/complete')
response.raise_for_status()
return response.json()
def delete_task(self, project_id, task_id):
"""Delete a task"""
response = self._request('DELETE', f'task/{project_id}/{task_id}')
response.raise_for_status()
return True
class HealthMarriageTracker:
"""Health & Marriage tracking system using TickTick REST API"""
def __init__(self):
# Get credentials from environment
client_id = os.getenv('TICKTICK_CLIENT_ID')
client_secret = os.getenv('TICKTICK_CLIENT_SECRET')
redirect_uri = os.getenv('TICKTICK_REDIRECT_URI', 'http://127.0.0.1:8080')
if not client_id or not client_secret:
raise ValueError(
"Required environment variables:\n"
" TICKTICK_CLIENT_ID\n"
" TICKTICK_CLIENT_SECRET\n"
"Get these from: https://developer.ticktick.com/manage"
)
self.client = TickTickClient(client_id, client_secret, redirect_uri)
# Ensure we're authenticated
if not self.client.access_token:
print("No existing token found. Starting OAuth flow...\n")
self.client.authorize()
self.projects = {}
def setup_complete_system(self):
"""Create entire tracking system in TickTick"""
print("\n" + "=" * 60)
print("Setting up Health & Marriage Tracking System")
print("=" * 60)
# Create projects
print("\nCreating projects...")
self.projects['Marriage'] = self.client.create_project("Marriage", "#E91E63")
self.projects['Health'] = self.client.create_project("Health", "#4CAF50")
self.projects['Milestones'] = self.client.create_project("Milestones", "#FF9800")
print("ā Projects created")
# Create marriage tasks
print("\nCreating Marriage tasks...")
self._create_marriage_tasks()
# Create health tasks
print("\nCreating Health tasks...")
self._create_health_tasks()
# Create milestone tasks
print("\nCreating Milestone tasks...")
self._create_milestone_tasks()
print("\n" + "=" * 60)
print("ā Setup Complete!")
print("=" * 60)
print("\nYour TickTick is now configured with:")
print(" ⢠Marriage project with weekly check-ins")
print(" ⢠Health project with daily/weekly tracking")
print(" ⢠Milestone project with 30/90/180-day targets")
print("\nOpen TickTick to start tracking!")
def _create_marriage_tasks(self):
"""Create marriage-related tasks - Week 1 specific actions"""
project_id = self.projects['Marriage']['id']
# Get next occurrence of each day
today = datetime.now()
# Monday: Download app
next_monday = self._get_next_weekday(0) # 0 = Monday
self.client.create_task(
title="Download Blueheart App",
content="""Download Blueheart from App Store
- Research-backed intimacy app
- Based on Sensate Focus therapy (gold standard)
- 83% effective in clinical studies
- Addresses body image, communication, desire mismatches
Why: Science shows structured intimacy reduces cortisol (stress) and increases connection.""",
projectId=project_id,
dueDate=next_monday.strftime("%Y-%m-%dT09:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Download Blueheart App (Monday)")
# Tuesday: Pitch to Nicole
next_tuesday = self._get_next_weekday(1) # 1 = Tuesday
self.client.create_task(
title="Pitch Blueheart to Nicole",
content="""Have the conversation:
"I found a research-backed intimacy app called Blueheart. It's like couples therapy on your phone - guides you through timed touch exercises. Science shows it reduces stress and increases connection. Want to try one 30-minute session together this week?"
Be ready to:
- Show her the app
- Explain it's structured/guided (not awkward guessing)
- Emphasize: helps with body confidence and communication
- No pressure - just one session to try
Goal: Schedule a specific day/time for the session.""",
projectId=project_id,
dueDate=next_tuesday.strftime("%Y-%m-%dT19:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Pitch Blueheart to Nicole (Tuesday)")
# Friday: Do Blueheart session (placeholder - adjust day as needed)
next_friday = self._get_next_weekday(4) # 4 = Friday
self.client.create_task(
title="Blueheart Guided Session (30 min)",
content="""Do the first Blueheart guided session together:
- 30 minutes, phones off, no interruptions
- Let the app guide you - don't overthink it
- Focus on being present, not performance
- Goal: Connection, not orgasm
Remember:
- This is an experiment
- Awkward is normal the first time
- Science shows this reduces stress for both of you
- You're rebuilding foundation together
Adjust this task's date to whatever day you and Nicole agreed on.""",
projectId=project_id,
dueDate=next_friday.strftime("%Y-%m-%dT20:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Blueheart Session (Friday - adjust as needed)")
# Sunday: Review
next_sunday = self._get_next_sunday(20, 0) # 8pm
self.client.create_task(
title="Week 1 Marriage Review",
content="""Debrief the week:
- Did you do the Blueheart session?
- How did it feel? Awkward? Good? Weird?
- Did Nicole feel safe/comfortable?
- Want to try again next week?
No judgment - first time is always weird.
Track:
- Connection this week (1-10): ___
- Intimacy this week: ___ times
- What worked: ___
- What to adjust: ___
Next week: Another Blueheart session or different approach?""",
projectId=project_id,
dueDate=next_sunday.strftime("%Y-%m-%dT20:00:00+0000"),
timeZone="America/Los_Angeles",
priority=3
)
print(" ā Week 1 Marriage Review (Sunday)")
def _create_health_tasks(self):
"""Create health-related tasks - Week 1 spine-safe plan"""
project_id = self.projects['Health']['id']
# Monday: Strength/Stretch
next_monday = self._get_next_weekday(0)
self.client.create_task(
title="Monday: 30 min Strength/Stretch",
content="""SPINE-SAFE ROUTINE:
Warm-up (5 min):
- Light movement, arm circles, gentle twists
Stretches (15 min):
- Cat/cow stretch: 10 reps (~2 min)
- Child's pose: Hold 60 seconds
- Hip flexor stretch: 30 sec each side
- Hamstring stretch: 30 sec each side
- Spinal twist: 30 sec each side
- Piriformis stretch: 30 sec each side
- Lower back stretch: 60 seconds
- Repeat circuit 2x
Strength (10 min):
- Bodyweight squats: 3 sets of 10
- Plank: 3 sets, 20-30 sec each
- Bird dogs: 3 sets of 10 each side
- Glute bridges: 3 sets of 12
- Rest 30 sec between sets
Track: How does spine feel after? (1-10)""",
projectId=project_id,
dueDate=next_monday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Monday: Strength/Stretch")
# Tuesday: Rest/Walk
next_tuesday = self._get_next_weekday(1)
self.client.create_task(
title="Tuesday: Rest or Light Walk",
content="""Optional: 20-minute easy walk
- Listen to your body
- If spine feels tight, walk
- If tired, rest completely
- Track mood: ___/10""",
projectId=project_id,
dueDate=next_tuesday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=1
)
print(" ā Tuesday: Rest/Walk")
# Wednesday: Cycling
next_wednesday = self._get_next_weekday(2)
self.client.create_task(
title="Wednesday: Cycling 45 min",
content="""Indoor trainer session:
- 5 min warm-up (easy spin)
- 35 min Zone 2 (conversational pace)
- 5 min cool down
Zone 2 check: Can you talk in full sentences?
Track:
- Mood after: ___/10
- Spine feel: ___/10""",
projectId=project_id,
dueDate=next_wednesday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Wednesday: Cycling")
# Thursday: Strength/Stretch
next_thursday = self._get_next_weekday(3)
self.client.create_task(
title="Thursday: 30 min Strength/Stretch",
content="""SPINE-SAFE ROUTINE (same as Monday):
Warm-up (5 min):
- Light movement, arm circles, gentle twists
Stretches (15 min):
- Cat/cow stretch: 10 reps (~2 min)
- Child's pose: Hold 60 seconds
- Hip flexor stretch: 30 sec each side
- Hamstring stretch: 30 sec each side
- Spinal twist: 30 sec each side
- Piriformis stretch: 30 sec each side
- Lower back stretch: 60 seconds
- Repeat circuit 2x
Strength (10 min):
- Bodyweight squats: 3 sets of 10
- Plank: 3 sets, 20-30 sec each
- Bird dogs: 3 sets of 10 each side
- Glute bridges: 3 sets of 12
- Rest 30 sec between sets
Track: How does spine feel after? (1-10)""",
projectId=project_id,
dueDate=next_thursday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Thursday: Strength/Stretch")
# Friday: Rest/Walk
next_friday = self._get_next_weekday(4)
self.client.create_task(
title="Friday: Rest or Light Walk",
content="""Optional: 20-minute easy walk
- Listen to your body
- If spine feels tight, walk
- If tired, rest completely
- Track mood: ___/10""",
projectId=project_id,
dueDate=next_friday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=1
)
print(" ā Friday: Rest/Walk")
# Saturday: Cycling
next_saturday = self._get_next_weekday(5)
self.client.create_task(
title="Saturday: Cycling 45 min",
content="""Indoor trainer session:
- 5 min warm-up (easy spin)
- 35 min Zone 2 (conversational pace)
- 5 min cool down
Zone 2 check: Can you talk in full sentences?
Track:
- Mood after: ___/10
- Spine feel: ___/10""",
projectId=project_id,
dueDate=next_saturday.strftime("%Y-%m-%dT07:00:00+0000"),
timeZone="America/Los_Angeles",
priority=5
)
print(" ā Saturday: Cycling")
# Sunday: Weekly Check-in
next_sunday = self._get_next_sunday(8, 0)
self.client.create_task(
title="Sunday: Weekly Health Check-in",
content="""Morning weigh-in:
- Same time, same scale
- Weight: _____
- Track trend, not daily fluctuation
Week 1 Review:
- Exercise days completed: ___/6 planned
- 2x cycling? Yes/No
- 2x strength/stretch? Yes/No
- Average mood: ___/10
- Spine feeling: Better/Same/Worse
- Energy level: ___/10
What worked:
What to adjust for Week 2:
Goal: Consistency over intensity. Did you show up?""",
projectId=project_id,
dueDate=next_sunday.strftime("%Y-%m-%dT08:00:00+0000"),
timeZone="America/Los_Angeles",
priority=3
)
print(" ā Sunday: Weekly Health Check-in")
def _create_milestone_tasks(self):
"""Create milestone tracking tasks"""
project_id = self.projects['Milestones']['id']
# 30-day milestone
due_30 = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%dT09:00:00+0000")
self.client.create_task(
title="30-Day Milestone Check",
content="""30-Day Targets:
Marriage:
- [ ] 4 consecutive marriage check-ins
- [ ] Intimacy: 2x/week average
- [ ] Connection score: 6 ā 7
Health:
- [ ] Weight: -5 lbs (230 ā 225)
- [ ] Exercise: 20/30 days
- [ ] Mood: Average 6+""",
projectId=project_id,
dueDate=due_30,
timeZone="America/Los_Angeles",
priority=5
)
print(" ā 30-Day Milestone")
# 90-day milestone
due_90 = (datetime.now() + timedelta(days=90)).strftime("%Y-%m-%dT09:00:00+0000")
self.client.create_task(
title="90-Day Milestone Check",
content="""90-Day Targets:
Marriage:
- [ ] 12 consecutive check-ins
- [ ] Marriage quality: 8/10
- [ ] Depression days: <5/month
Health:
- [ ] Weight: -15 lbs (230 ā 215)
- [ ] Run 5K without stopping
- [ ] Exercise habit established""",
projectId=project_id,
dueDate=due_90,
timeZone="America/Los_Angeles",
priority=5
)
print(" ā 90-Day Milestone")
# 180-day vision
due_180 = (datetime.now() + timedelta(days=180)).strftime("%Y-%m-%dT09:00:00+0000")
self.client.create_task(
title="6-Month Vision Check",
content="""6-Month Vision:
- [ ] Weight: 195 lbs
- [ ] 10K capability restored
- [ ] Marriage: Consistent 8+/10
- [ ] Aging together, not alone
- [ ] Stress managed proactively""",
projectId=project_id,
dueDate=due_180,
timeZone="America/Los_Angeles",
priority=5
)
print(" ā 6-Month Vision")
def _get_next_sunday(self, hour=0, minute=0):
"""Get next Sunday at specified time"""
today = datetime.now()
days_until_sunday = (6 - today.weekday()) % 7
if days_until_sunday == 0 and today.hour >= hour:
days_until_sunday = 7
next_sunday = today + timedelta(days=days_until_sunday)
return next_sunday.replace(hour=hour, minute=minute, second=0, microsecond=0)
def _get_next_weekday(self, weekday, hour=7, minute=0):
"""Get next occurrence of specified weekday (0=Mon, 6=Sun)"""
today = datetime.now()
days_ahead = weekday - today.weekday()
if days_ahead <= 0: # Target day already happened this week
days_ahead += 7
next_day = today + timedelta(days=days_ahead)
return next_day.replace(hour=hour, minute=minute, second=0, microsecond=0)
def view_status(self):
"""View current tasks and projects"""
print("\n" + "=" * 60)
print("Current Status")
print("=" * 60)
projects = self.client.get_projects()
tasks = self.client.get_tasks()
# Group tasks by project
by_project = {}
for task in tasks:
pid = task.get('projectId', 'inbox')
if pid not in by_project:
by_project[pid] = []
by_project[pid].append(task)
# Display
for project in projects:
pid = project['id']
pname = project['name']
ptasks = by_project.get(pid, [])
print(f"\n{pname} ({len(ptasks)} tasks):")
for task in ptasks[:5]: # Show first 5
status = "ā" if task.get('status') == 2 else "ā"
print(f" {status} {task['title']}")
def main():
import argparse
parser = argparse.ArgumentParser(
description="Health & Marriage TickTick Tracking System (REST API)"
)
parser.add_argument(
'--setup',
action='store_true',
help='Initial setup - create all projects and tasks'
)
parser.add_argument(
'--status',
action='store_true',
help='View current status'
)
parser.add_argument(
'--auth',
action='store_true',
help='Test authentication only'
)
parser.add_argument(
'--test',
action='store_true',
help='Create one test task to verify API works'
)
args = parser.parse_args()
try:
tracker = HealthMarriageTracker()
if args.setup:
tracker.setup_complete_system()
elif args.status:
tracker.view_status()
elif args.auth:
print("\nā Authentication successful!")
print(f"Access token: {tracker.client.access_token[:20]}...")
elif args.test:
print("\n" + "=" * 60)
print("Creating Test Task")
print("=" * 60)
# Create one simple task
task = tracker.client.create_task(
title="šÆ Test Task - Health & Marriage Tracker",
content="If you can see this task in TickTick, the API is working perfectly!",
priority=1
)
print(f"\nā Test task created successfully!")
print(f" Task ID: {task['id']}")
print(f" Title: {task['title']}")
print(f"\nš Check your TickTick Inbox - you should see the test task!")
print("=" * 60)
else:
parser.print_help()
except Exception as e:
print(f"\nā Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()